@nextclaw/x-harness 0.1.0 → 0.2.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/LICENSE +231 -6
- package/README.md +17 -3
- package/SOURCE.md +9 -0
- package/cli/main.js +232 -188
- package/dist/api.d.ts +56 -2
- package/dist/api.js +178 -75
- package/dist/boundary.js +1 -0
- package/dist/browser-common.d.ts +11 -0
- package/dist/browser-common.js +34 -18
- package/dist/browser-dom-capabilities.d.ts +9 -0
- package/dist/browser-dom-capabilities.js +131 -0
- package/dist/browser-dom-dependencies.d.ts +16 -0
- package/dist/browser-dom-dependencies.js +1 -0
- package/dist/browser-dom-editing.d.ts +166 -0
- package/dist/browser-dom-editing.js +418 -0
- package/dist/browser-dom-navigation.d.ts +18 -0
- package/dist/browser-dom-navigation.js +181 -0
- package/dist/browser-dom-network.d.ts +1 -0
- package/dist/browser-dom-network.js +128 -0
- package/dist/browser-dom-submission.d.ts +7 -0
- package/dist/browser-dom-submission.js +101 -0
- package/dist/browser-dom.d.ts +14 -2
- package/dist/browser-dom.js +120 -824
- package/dist/browser-native.d.ts +14 -1
- package/dist/browser-native.js +180 -143
- package/dist/browser.js +6 -2
- package/dist/creator-native.d.ts +3 -0
- package/dist/creator-native.js +113 -0
- package/dist/creator.js +1 -1
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +10 -0
- package/dist/growth-batch.d.ts +8 -0
- package/dist/growth-batch.js +190 -0
- package/dist/growth-constants.d.ts +5 -0
- package/dist/growth-constants.js +13 -0
- package/dist/growth-dependencies.d.ts +33 -0
- package/dist/growth-dependencies.js +1 -0
- package/dist/growth-execution.d.ts +17 -0
- package/dist/growth-execution.js +346 -0
- package/dist/growth-parallel.d.ts +8 -0
- package/dist/growth-parallel.js +332 -0
- package/dist/growth-plan.d.ts +9 -0
- package/dist/growth-plan.js +229 -0
- package/dist/growth-policy.d.ts +18 -0
- package/dist/growth-policy.js +208 -0
- package/dist/growth-publishing.d.ts +10 -0
- package/dist/growth-publishing.js +266 -0
- package/dist/growth-reading.d.ts +8 -0
- package/dist/growth-reading.js +157 -0
- package/dist/growth-recovery.d.ts +9 -0
- package/dist/growth-recovery.js +187 -0
- package/dist/growth-state.d.ts +16 -0
- package/dist/growth-state.js +220 -0
- package/dist/growth-tasks.d.ts +20 -0
- package/dist/growth-tasks.js +164 -0
- package/dist/growth.d.ts +26 -1
- package/dist/growth.js +211 -1908
- package/dist/platform-capabilities.d.ts +8 -0
- package/dist/platform-capabilities.js +29 -0
- package/dist/platform-collections.d.ts +14 -0
- package/dist/platform-collections.js +250 -0
- package/dist/platform-dependencies.d.ts +43 -0
- package/dist/platform-dependencies.js +1 -0
- package/dist/platform-diagnostics.d.ts +14 -0
- package/dist/platform-diagnostics.js +63 -0
- package/dist/platform-observation.d.ts +16 -0
- package/dist/platform-observation.js +96 -0
- package/dist/platform-rules.d.ts +17 -0
- package/dist/platform-rules.js +41 -0
- package/dist/platform-targets.d.ts +14 -0
- package/dist/platform-targets.js +154 -0
- package/dist/platform-writing.d.ts +16 -0
- package/dist/platform-writing.js +202 -0
- package/dist/platform.d.ts +23 -2
- package/dist/platform.js +125 -678
- package/dist/readers-actions.d.ts +2 -0
- package/dist/readers-actions.js +66 -0
- package/dist/readers-ax-host.d.ts +12 -0
- package/dist/readers-ax-host.js +134 -0
- package/dist/readers-ax-parser.d.ts +1 -0
- package/dist/readers-ax-parser.js +69 -0
- package/dist/readers-ax-program.d.ts +1 -0
- package/dist/readers-ax-program.js +5 -0
- package/dist/readers-ax-tree.d.ts +19 -0
- package/dist/readers-ax-tree.js +111 -0
- package/dist/readers-ax-views.d.ts +87 -0
- package/dist/readers-ax-views.js +247 -0
- package/dist/readers-cli.d.ts +6 -0
- package/dist/readers-cli.js +79 -0
- package/dist/readers-composer.d.ts +2 -0
- package/dist/readers-composer.js +131 -0
- package/dist/readers-navigation.d.ts +2 -0
- package/dist/readers-navigation.js +66 -0
- package/dist/readers-posts.d.ts +2 -0
- package/dist/readers-posts.js +141 -0
- package/dist/readers-profile.d.ts +3 -0
- package/dist/readers-profile.js +157 -0
- package/dist/readers.d.ts +9 -20
- package/dist/readers.js +9 -1045
- package/dist/results.js +1 -1
- package/dist/round-state.js +20 -3
- package/dist/store.d.ts +22 -4
- package/dist/store.js +173 -151
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/generated/observation.mjs +3 -2
- package/package.json +23 -9
- package/skill/SKILL.md +17 -36
- package/skill/references/api.md +7 -34
- package/skill/references/reads.md +23 -0
- package/skill/references/recovery.md +25 -0
- package/skill/references/setup.md +23 -0
- package/skill/references/writes.md +51 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { followWindows, same } from "./growth-constants.js";
|
|
2
|
+
export class FollowBatch {
|
|
3
|
+
#prepareActionsForCall;
|
|
4
|
+
#store;
|
|
5
|
+
#scope;
|
|
6
|
+
#stamp;
|
|
7
|
+
#clock;
|
|
8
|
+
#evidenceExists;
|
|
9
|
+
#executor;
|
|
10
|
+
#parallelRound;
|
|
11
|
+
#round;
|
|
12
|
+
constructor(dependencies) {
|
|
13
|
+
this.followBatch = this.followBatch.bind(this);
|
|
14
|
+
const { prepareActionsForCall, store, scope, stamp, clock, evidenceExists, executor, parallelRound, round, } = dependencies;
|
|
15
|
+
this.#prepareActionsForCall = prepareActionsForCall;
|
|
16
|
+
this.#store = store;
|
|
17
|
+
this.#scope = scope;
|
|
18
|
+
this.#stamp = stamp;
|
|
19
|
+
this.#clock = clock;
|
|
20
|
+
this.#evidenceExists = evidenceExists;
|
|
21
|
+
this.#executor = executor;
|
|
22
|
+
this.#parallelRound = parallelRound;
|
|
23
|
+
this.#round = round;
|
|
24
|
+
}
|
|
25
|
+
async followBatch({ taskId, handles, preparedActions } = {}) {
|
|
26
|
+
const executor = this.#executor;
|
|
27
|
+
if (!Array.isArray(handles) ||
|
|
28
|
+
handles.length < 5 ||
|
|
29
|
+
handles.length > 10 ||
|
|
30
|
+
handles.some((h) => !/^[A-Za-z0-9_]{1,15}$/.test(h)) ||
|
|
31
|
+
new Set(handles.map((h) => h.toLowerCase())).size !== handles.length)
|
|
32
|
+
throw new Error("正常关注子批需5~10位不同的已准备对象");
|
|
33
|
+
if (preparedActions?.some((a) => a.kind !== "follow" ||
|
|
34
|
+
!handles.some((h) => same(h, a.handle))))
|
|
35
|
+
throw new Error("batch-action-target-mismatch");
|
|
36
|
+
await this.#prepareActionsForCall(taskId, preparedActions);
|
|
37
|
+
let batchId, resumedBatch = false;
|
|
38
|
+
const original = await this.#store.transaction(async (snapshot) => {
|
|
39
|
+
if (!this.#scope(snapshot.state).includes("follow"))
|
|
40
|
+
throw new Error("kind-outside-round-scope");
|
|
41
|
+
if (snapshot.state.batchId !== taskId ||
|
|
42
|
+
handles.some((h) => !snapshot.state.actions?.some((a) => a.kind === "follow" && same(a.handle, h))))
|
|
43
|
+
throw new Error("子批名单必须归原任务队列");
|
|
44
|
+
const next = structuredClone(snapshot.state);
|
|
45
|
+
next.followBatches ??= [];
|
|
46
|
+
const previous = next.followBatches.findLast((b) => b.status !== "SETTLED" &&
|
|
47
|
+
b.handles.length === handles.length &&
|
|
48
|
+
b.handles.every((h, i) => same(h, handles[i])));
|
|
49
|
+
resumedBatch = Boolean(previous);
|
|
50
|
+
batchId = previous?.id ?? `${taskId}-FOLLOW-${this.#stamp()}`;
|
|
51
|
+
if (previous)
|
|
52
|
+
Object.assign(previous, {
|
|
53
|
+
status: "RUNNING",
|
|
54
|
+
lastResumedAt: this.#stamp(),
|
|
55
|
+
});
|
|
56
|
+
else
|
|
57
|
+
next.followBatches.push({
|
|
58
|
+
id: batchId,
|
|
59
|
+
handles,
|
|
60
|
+
planned: handles.length,
|
|
61
|
+
startedAt: this.#stamp(),
|
|
62
|
+
status: "RUNNING",
|
|
63
|
+
});
|
|
64
|
+
next.updatedAt = this.#stamp();
|
|
65
|
+
return next;
|
|
66
|
+
});
|
|
67
|
+
const preparedCount = original.state.actions.filter((a) => a.kind === "follow" &&
|
|
68
|
+
a.status === "prepared" &&
|
|
69
|
+
handles.some((h) => same(h, a.handle))).length;
|
|
70
|
+
let eligibleAt = this.#clock();
|
|
71
|
+
const previousGroup = original.state.execution?.followGroup;
|
|
72
|
+
const lastAttempt = (original.state.attempts ?? [])
|
|
73
|
+
.filter((a) => a.kind === "follow")
|
|
74
|
+
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at))
|
|
75
|
+
.at(-1);
|
|
76
|
+
const lastAction = original.state.actions.find((a) => a.id === lastAttempt?.actionId);
|
|
77
|
+
const sameBatchRecovery = resumedBatch &&
|
|
78
|
+
lastAction?.status === "verified" &&
|
|
79
|
+
this.#evidenceExists(lastAction.resultEvidence) &&
|
|
80
|
+
handles.some((h) => same(h, lastAction.handle));
|
|
81
|
+
if (preparedCount && previousGroup?.attempts && !sameBatchRecovery)
|
|
82
|
+
eligibleAt = Math.max(eligibleAt, Date.parse(previousGroup.lastAt) + 180000);
|
|
83
|
+
for (const [duration, limit] of followWindows) {
|
|
84
|
+
const recent = (original.state.attempts ?? [])
|
|
85
|
+
.filter((a) => a.kind === "follow" && Date.parse(a.at) > this.#clock() - duration)
|
|
86
|
+
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
87
|
+
if (preparedCount > limit - recent.length)
|
|
88
|
+
eligibleAt = Math.max(eligibleAt, Date.parse(recent[recent.length - limit + preparedCount - 1].at) +
|
|
89
|
+
duration);
|
|
90
|
+
}
|
|
91
|
+
if (eligibleAt > this.#clock()) {
|
|
92
|
+
const resumeAt = new Date(eligibleAt).toISOString();
|
|
93
|
+
await this.#store.transaction((snapshot) => {
|
|
94
|
+
const next = structuredClone(snapshot.state);
|
|
95
|
+
Object.assign(next.followBatches.find((b) => b.id === batchId), { status: "WAITING", remaining: preparedCount, resumeAt });
|
|
96
|
+
next.updatedAt = this.#stamp();
|
|
97
|
+
return next;
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
taskId,
|
|
101
|
+
batchId,
|
|
102
|
+
planned: handles.length,
|
|
103
|
+
attempted: 0,
|
|
104
|
+
verifiedThisCall: 0,
|
|
105
|
+
unknownInBatch: original.state.actions.filter((a) => handles.some((h) => same(h, a.handle)) &&
|
|
106
|
+
["unknown", "intent"].includes(a.status)).length,
|
|
107
|
+
remaining: preparedCount,
|
|
108
|
+
mayEnd: false,
|
|
109
|
+
decision: [
|
|
110
|
+
{
|
|
111
|
+
kind: "follow",
|
|
112
|
+
status: "waiting",
|
|
113
|
+
reason: "normal-batch-capacity-or-cooldown",
|
|
114
|
+
resumeAt,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
evidenceRef: this.#store.path,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
let result;
|
|
121
|
+
const exceptional = [];
|
|
122
|
+
for (const handle of handles) {
|
|
123
|
+
// A resumed batch submits only its remaining prepared members. Recovery
|
|
124
|
+
// stays in the read-only API; a frozen member must not disturb that path.
|
|
125
|
+
if (!original.state.actions.some((a) => a.kind === "follow" &&
|
|
126
|
+
same(a.handle, handle) &&
|
|
127
|
+
a.status === "prepared"))
|
|
128
|
+
continue;
|
|
129
|
+
result = await (executor ? this.#parallelRound : this.#round)({
|
|
130
|
+
taskId,
|
|
131
|
+
requestedKind: "follow",
|
|
132
|
+
selector: (a) => a.kind === "follow" && same(a.handle, handle),
|
|
133
|
+
});
|
|
134
|
+
const relevant = (result.decision ?? []).filter((d) => d.kind === "follow" ||
|
|
135
|
+
["harness-boundary-error", "original-ledger-conflict"].includes(d.reason) ||
|
|
136
|
+
original.state.actions.some((a) => a.id === d.actionId && a.kind === "follow"));
|
|
137
|
+
exceptional.push(...relevant);
|
|
138
|
+
// A new unconfirmed submission is a diagnostic boundary, not permission
|
|
139
|
+
// to consume the rest of the batch on the same failing write path.
|
|
140
|
+
if (relevant.length)
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
const after = await this.#store.load();
|
|
144
|
+
const summary = summarizeFollowBatch(taskId, batchId, handles, after, original, result, exceptional, this.#store.path);
|
|
145
|
+
await this.#store.transaction((snapshot) => {
|
|
146
|
+
const next = structuredClone(snapshot.state), batch = next.followBatches.find((b) => b.id === batchId);
|
|
147
|
+
Object.assign(batch, {
|
|
148
|
+
attempted: (batch.attempted ?? 0) + summary.attempted,
|
|
149
|
+
verifiedTotal: (batch.verifiedTotal ?? 0) + summary.verifiedThisCall,
|
|
150
|
+
verifiedThisCall: summary.verifiedThisCall,
|
|
151
|
+
unknown: summary.unknownInBatch,
|
|
152
|
+
remaining: summary.remaining,
|
|
153
|
+
finishedAt: this.#stamp(),
|
|
154
|
+
status: summary.remaining || summary.unknownInBatch ? "PARTIAL" : "SETTLED",
|
|
155
|
+
});
|
|
156
|
+
next.updatedAt = this.#stamp();
|
|
157
|
+
return next;
|
|
158
|
+
});
|
|
159
|
+
return summary;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function createFollowBatch(...args) {
|
|
163
|
+
return new FollowBatch(...args);
|
|
164
|
+
}
|
|
165
|
+
function summarizeFollowBatch(taskId, batchId, handles, after, original, result, exceptional, evidenceRef) {
|
|
166
|
+
return {
|
|
167
|
+
taskId,
|
|
168
|
+
batchId,
|
|
169
|
+
planned: handles.length,
|
|
170
|
+
attempted: (after.state.attempts ?? [])
|
|
171
|
+
.slice(original.state.attempts?.length ?? 0)
|
|
172
|
+
.filter((a) => a.kind === "follow" &&
|
|
173
|
+
handles.some((h) => same(after.state.actions.find((x) => x.id === a.actionId)?.handle, h))).length,
|
|
174
|
+
verifiedThisCall: after.state.actions.filter((a) => a.kind === "follow" &&
|
|
175
|
+
a.status === "verified" &&
|
|
176
|
+
handles.some((h) => same(h, a.handle)) &&
|
|
177
|
+
original.state.actions.find((old) => old.id === a.id)?.status !==
|
|
178
|
+
"verified").length,
|
|
179
|
+
unknownInBatch: after.state.actions.filter((a) => a.kind === "follow" &&
|
|
180
|
+
handles.some((h) => same(a.handle, h)) &&
|
|
181
|
+
["unknown", "intent"].includes(a.status)).length,
|
|
182
|
+
remaining: after.state.actions.filter((a) => a.kind === "follow" &&
|
|
183
|
+
handles.some((h) => same(a.handle, h)) &&
|
|
184
|
+
a.status === "prepared").length,
|
|
185
|
+
actual: result?.actual,
|
|
186
|
+
decision: exceptional,
|
|
187
|
+
mayEnd: result?.mayEnd ?? false,
|
|
188
|
+
evidenceRef,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const kinds = ["follow", "original", "reply"];
|
|
2
|
+
export const followWindows = [
|
|
3
|
+
[600000, 20],
|
|
4
|
+
[3600000, 100],
|
|
5
|
+
];
|
|
6
|
+
export const operations = {
|
|
7
|
+
follow: "submitFollow",
|
|
8
|
+
original: "submitPost",
|
|
9
|
+
reply: "submitReply",
|
|
10
|
+
};
|
|
11
|
+
export const same = (a, b) => typeof a === "string" &&
|
|
12
|
+
typeof b === "string" &&
|
|
13
|
+
a.toLowerCase() === b.toLowerCase();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BoundaryRecord } from "./boundary.js";
|
|
2
|
+
import type { RoundStore } from "./store.js";
|
|
3
|
+
/** Explicit instance dependencies; each owner accepts only its declared subset. */
|
|
4
|
+
export interface GrowthDependencies {
|
|
5
|
+
platform: BoundaryRecord;
|
|
6
|
+
store: RoundStore;
|
|
7
|
+
clock: () => number;
|
|
8
|
+
executor: BoundaryRecord | undefined;
|
|
9
|
+
stamp: () => string;
|
|
10
|
+
evidenceExists: (path: string) => boolean;
|
|
11
|
+
runtimeId: string;
|
|
12
|
+
session: {
|
|
13
|
+
claim: any;
|
|
14
|
+
running: boolean;
|
|
15
|
+
};
|
|
16
|
+
authorizedWindow: (state: any, kind: string) => boolean;
|
|
17
|
+
scope: (state: any) => any;
|
|
18
|
+
project: (state: any) => any;
|
|
19
|
+
load: (taskId: any) => Promise<any>;
|
|
20
|
+
save: (snapshot: any, state: any) => Promise<any>;
|
|
21
|
+
result: (snapshot: any, needs?: any) => any;
|
|
22
|
+
capability: (operation: any, input?: any) => any;
|
|
23
|
+
waitBudget: (state: any) => any;
|
|
24
|
+
guardInput: (state: any, action: any, independentExecution?: any) => any;
|
|
25
|
+
settleProof: (snapshot: any, action: any, observation: any, { preserveStatus }?: any) => Promise<any>;
|
|
26
|
+
execute: (snapshot: any, action: any) => Promise<any>;
|
|
27
|
+
parallelRound: ({ taskId, decision, selector, requestedKind, maxActions, }?: any) => Promise<any>;
|
|
28
|
+
round: ({ taskId, decision, selector, requestedKind }?: any) => Promise<any>;
|
|
29
|
+
publishIndependent: ({ taskId, draftPath, preparedAction, independentExecution, }: any) => Promise<any>;
|
|
30
|
+
releaseSession: (taskId?: any) => Promise<any>;
|
|
31
|
+
readTask: (operation: any, { mode, ...input }?: any) => Promise<any>;
|
|
32
|
+
prepareActionsForCall: (taskId: string, actions?: any[]) => Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BoundaryRecord } from "./boundary.js";
|
|
2
|
+
import type { RoundSnapshot } from "./store.js";
|
|
3
|
+
interface ExecutionStep {
|
|
4
|
+
snapshot: RoundSnapshot;
|
|
5
|
+
need: BoundaryRecord | null;
|
|
6
|
+
}
|
|
7
|
+
import type { GrowthDependencies } from "./growth-dependencies.js";
|
|
8
|
+
export declare class GrowthExecution {
|
|
9
|
+
#private;
|
|
10
|
+
execute: (snapshot: any, action: any) => Promise<ExecutionStep>;
|
|
11
|
+
constructor(dependencies: Pick<GrowthDependencies, "capability" | "platform" | "settleProof" | "clock" | "save" | "guardInput" | "waitBudget" | "authorizedWindow" | "stamp" | "store" | "executor" | "session">);
|
|
12
|
+
private checkExecutionIdentity;
|
|
13
|
+
private admitAction;
|
|
14
|
+
private handleExecute;
|
|
15
|
+
}
|
|
16
|
+
export declare function createGrowthExecution(...args: ConstructorParameters<typeof GrowthExecution>): GrowthExecution;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { operations, same } from "./growth-constants.js";
|
|
2
|
+
export class GrowthExecution {
|
|
3
|
+
#capability;
|
|
4
|
+
#platform;
|
|
5
|
+
#settleProof;
|
|
6
|
+
#clock;
|
|
7
|
+
#save;
|
|
8
|
+
#guardInput;
|
|
9
|
+
#waitBudget;
|
|
10
|
+
#authorizedWindow;
|
|
11
|
+
#stamp;
|
|
12
|
+
#store;
|
|
13
|
+
#executor;
|
|
14
|
+
#session;
|
|
15
|
+
execute;
|
|
16
|
+
constructor(dependencies) {
|
|
17
|
+
const { capability, platform, settleProof, clock, save, guardInput, waitBudget, authorizedWindow, stamp, store, executor, session, } = dependencies;
|
|
18
|
+
this.#capability = capability;
|
|
19
|
+
this.#platform = platform;
|
|
20
|
+
this.#settleProof = settleProof;
|
|
21
|
+
this.#clock = clock;
|
|
22
|
+
this.#save = save;
|
|
23
|
+
this.#guardInput = guardInput;
|
|
24
|
+
this.#waitBudget = waitBudget;
|
|
25
|
+
this.#authorizedWindow = authorizedWindow;
|
|
26
|
+
this.#stamp = stamp;
|
|
27
|
+
this.#store = store;
|
|
28
|
+
this.#executor = executor;
|
|
29
|
+
this.#session = session;
|
|
30
|
+
this.execute = this.handleExecute.bind(this);
|
|
31
|
+
}
|
|
32
|
+
async checkExecutionIdentity(snapshot, action) {
|
|
33
|
+
const state = snapshot.state;
|
|
34
|
+
let own;
|
|
35
|
+
try {
|
|
36
|
+
own = await this.#platform.readAccount();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
snapshot,
|
|
41
|
+
need: {
|
|
42
|
+
status: "needsDecision",
|
|
43
|
+
actionId: action.id,
|
|
44
|
+
reason: "account-read-failed",
|
|
45
|
+
error: error.message.slice(0, 240),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (own.needsStateCheck ||
|
|
50
|
+
own.alerts?.length ||
|
|
51
|
+
!same(own.accountHandle, state.accountHandle))
|
|
52
|
+
return {
|
|
53
|
+
snapshot,
|
|
54
|
+
need: {
|
|
55
|
+
status: "needsDecision",
|
|
56
|
+
actionId: action.id,
|
|
57
|
+
reason: "fresh-account-unconfirmed",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async admitAction(snapshot, action) {
|
|
63
|
+
const state = snapshot.state;
|
|
64
|
+
const item = state.items.find((i) => i.kind === action.kind);
|
|
65
|
+
if (!item.actionable)
|
|
66
|
+
return {
|
|
67
|
+
snapshot,
|
|
68
|
+
need: {
|
|
69
|
+
status: "needsDecision",
|
|
70
|
+
actionId: action.id,
|
|
71
|
+
reason: item.reason ?? "original-kind-not-actionable",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const inputError = this.#guardInput(state, action);
|
|
75
|
+
if (inputError)
|
|
76
|
+
return {
|
|
77
|
+
snapshot,
|
|
78
|
+
need: {
|
|
79
|
+
status: "needsDecision",
|
|
80
|
+
actionId: action.id,
|
|
81
|
+
reason: inputError,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const identity = await this.checkExecutionIdentity(snapshot, action);
|
|
85
|
+
if (identity)
|
|
86
|
+
return identity;
|
|
87
|
+
if (action.kind === "follow") {
|
|
88
|
+
const budget = this.#waitBudget(state);
|
|
89
|
+
if (budget)
|
|
90
|
+
return { snapshot, need: { ...budget, actionId: action.id } };
|
|
91
|
+
let value;
|
|
92
|
+
try {
|
|
93
|
+
value = await this.#platform.readPerson(action);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
snapshot,
|
|
98
|
+
need: {
|
|
99
|
+
status: "needsDecision",
|
|
100
|
+
actionId: action.id,
|
|
101
|
+
reason: "person-read-failed",
|
|
102
|
+
error: error.message.slice(0, 240),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (value.needsStateCheck ||
|
|
107
|
+
value.alerts?.length ||
|
|
108
|
+
value.relationship === "unknown" ||
|
|
109
|
+
!same(value.accountHandle, state.accountHandle))
|
|
110
|
+
return {
|
|
111
|
+
snapshot,
|
|
112
|
+
need: {
|
|
113
|
+
status: "needsDecision",
|
|
114
|
+
actionId: action.id,
|
|
115
|
+
reason: "fresh-person-unconfirmed",
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
if (value.blue === false)
|
|
119
|
+
return {
|
|
120
|
+
snapshot,
|
|
121
|
+
need: {
|
|
122
|
+
status: "needsDecision",
|
|
123
|
+
actionId: action.id,
|
|
124
|
+
reason: "fresh-blue-qualification-contradicts-review",
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
if (value.relationship === "following") {
|
|
128
|
+
const next = structuredClone(state);
|
|
129
|
+
next.actions.find((a) => a.id === action.id).status =
|
|
130
|
+
"preexisting";
|
|
131
|
+
return { snapshot: await this.#save(snapshot, next), need: null };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const hour = Number(new Intl.DateTimeFormat("en-GB", {
|
|
136
|
+
timeZone: "Asia/Shanghai",
|
|
137
|
+
hour: "2-digit",
|
|
138
|
+
hourCycle: "h23",
|
|
139
|
+
}).format(this.#clock()));
|
|
140
|
+
if ((hour < 7 || hour >= (action.kind === "reply" ? 24 : 22)) &&
|
|
141
|
+
!this.#authorizedWindow(state, action.kind))
|
|
142
|
+
return {
|
|
143
|
+
snapshot,
|
|
144
|
+
need: {
|
|
145
|
+
status: "needsDecision",
|
|
146
|
+
actionId: action.id,
|
|
147
|
+
reason: "outside-contract-write-window",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
let beforeIds = [];
|
|
152
|
+
if (action.kind !== "follow") {
|
|
153
|
+
let baseline;
|
|
154
|
+
try {
|
|
155
|
+
baseline = await this.#platform.findSubmission({
|
|
156
|
+
...action,
|
|
157
|
+
intentAt: this.#stamp(),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
snapshot,
|
|
163
|
+
need: {
|
|
164
|
+
status: "needsDecision",
|
|
165
|
+
actionId: action.id,
|
|
166
|
+
reason: "publication-baseline-read-failed",
|
|
167
|
+
error: error.message.slice(0, 240),
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (baseline.needsStateCheck || baseline.alerts?.length)
|
|
172
|
+
return {
|
|
173
|
+
snapshot,
|
|
174
|
+
need: {
|
|
175
|
+
status: "needsDecision",
|
|
176
|
+
actionId: action.id,
|
|
177
|
+
reason: "publication-baseline-unconfirmed",
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
beforeIds = (baseline.posts ?? []).map((p) => p.id).filter(Boolean);
|
|
181
|
+
}
|
|
182
|
+
return { beforeIds };
|
|
183
|
+
}
|
|
184
|
+
async handleExecute(snapshot, action) {
|
|
185
|
+
const executor = this.#executor;
|
|
186
|
+
const recovering = ["intent", "unknown"].includes(action.status);
|
|
187
|
+
const caps = this.#capability(recovering ? "read" : operations[action.kind], action);
|
|
188
|
+
if (!caps.covered)
|
|
189
|
+
return {
|
|
190
|
+
snapshot,
|
|
191
|
+
need: { status: "needsCapability", actionId: action.id, ...caps },
|
|
192
|
+
};
|
|
193
|
+
// Recovery is read-only even outside the write window. Never retry an intent.
|
|
194
|
+
if (recovering) {
|
|
195
|
+
let observation;
|
|
196
|
+
try {
|
|
197
|
+
observation = await this.#platform.findSubmission(action);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
observation = {
|
|
201
|
+
verified: false,
|
|
202
|
+
missing: ["dedup-read-failed"],
|
|
203
|
+
error: error.message.slice(0, 240),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
snapshot = await this.#settleProof(snapshot, action, observation);
|
|
207
|
+
return {
|
|
208
|
+
snapshot,
|
|
209
|
+
need: observation.verified
|
|
210
|
+
? null
|
|
211
|
+
: {
|
|
212
|
+
status: "needsDecision",
|
|
213
|
+
actionId: action.id,
|
|
214
|
+
reason: "submission-unknown-do-not-resubmit",
|
|
215
|
+
accountAlert: Boolean(observation.alerts?.length),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (snapshot.state.items
|
|
220
|
+
.find((i) => i.kind === action.kind)
|
|
221
|
+
.reason?.startsWith("harness:") &&
|
|
222
|
+
Date.parse(snapshot.state.items.find((i) => i.kind === action.kind).resumeAt) <= this.#clock()) {
|
|
223
|
+
const next = structuredClone(snapshot.state), pending = next.items.find((i) => i.kind === action.kind);
|
|
224
|
+
pending.actionable = true;
|
|
225
|
+
delete pending.reason;
|
|
226
|
+
delete pending.resumeAt;
|
|
227
|
+
delete pending.resumeWhen;
|
|
228
|
+
delete pending.blockEvidence;
|
|
229
|
+
next.status = "RUNNING";
|
|
230
|
+
snapshot = await this.#save(snapshot, next);
|
|
231
|
+
}
|
|
232
|
+
const admission = await this.admitAction(snapshot, action);
|
|
233
|
+
if ("snapshot" in admission)
|
|
234
|
+
return admission;
|
|
235
|
+
const { beforeIds } = admission;
|
|
236
|
+
const state = snapshot.state;
|
|
237
|
+
// Persist intent BEFORE any externally visible submission. Even a failed attempt consumes budget.
|
|
238
|
+
const next = structuredClone(state);
|
|
239
|
+
const entry = next.actions.find((a) => a.id === action.id);
|
|
240
|
+
entry.status = "intent";
|
|
241
|
+
entry.intentAt = this.#stamp();
|
|
242
|
+
entry.beforeIds = beforeIds;
|
|
243
|
+
next.items.find((i) => i.kind === action.kind).unknown += 1;
|
|
244
|
+
next.attempts ??= [];
|
|
245
|
+
next.attempts.push({
|
|
246
|
+
actionId: action.id,
|
|
247
|
+
kind: action.kind,
|
|
248
|
+
at: entry.intentAt,
|
|
249
|
+
});
|
|
250
|
+
if (action.kind === "follow") {
|
|
251
|
+
next.execution ??= {};
|
|
252
|
+
const old = next.execution.followGroup;
|
|
253
|
+
next.execution.followGroup = {
|
|
254
|
+
attempts: old?.attempts < 10 && this.#clock() < Date.parse(old.lastAt) + 180000
|
|
255
|
+
? old.attempts + 1
|
|
256
|
+
: 1,
|
|
257
|
+
lastAt: entry.intentAt,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
snapshot = await this.#save(snapshot, next);
|
|
261
|
+
let observation, submissionMayHaveStarted = false;
|
|
262
|
+
try {
|
|
263
|
+
await this.#platform[operations[action.kind]]({
|
|
264
|
+
...entry,
|
|
265
|
+
beforeSubmit: async () => {
|
|
266
|
+
const latest = await this.#store.load();
|
|
267
|
+
if (executor) {
|
|
268
|
+
const current = latest.state.actions.find((a) => a.id === entry.id);
|
|
269
|
+
if (latest.state.batchId !== snapshot.state.batchId ||
|
|
270
|
+
current?.claim?.token !== this.#session.claim.token ||
|
|
271
|
+
JSON.stringify(current) !==
|
|
272
|
+
JSON.stringify(snapshot.state.actions.find((a) => a.id === entry.id)))
|
|
273
|
+
throw new Error("提交前本动作或claim变化");
|
|
274
|
+
}
|
|
275
|
+
else if (latest.revision !== snapshot.revision)
|
|
276
|
+
throw new Error("提交前原台账变化,保留intent并重核执行权");
|
|
277
|
+
const inputError = this.#guardInput(latest.state, latest.state.actions.find((a) => a.id === entry.id));
|
|
278
|
+
if (inputError)
|
|
279
|
+
throw new Error(inputError);
|
|
280
|
+
const hour = Number(new Intl.DateTimeFormat("en-GB", {
|
|
281
|
+
timeZone: "Asia/Shanghai",
|
|
282
|
+
hour: "2-digit",
|
|
283
|
+
hourCycle: "h23",
|
|
284
|
+
}).format(this.#clock()));
|
|
285
|
+
if ((hour < 7 || hour >= (action.kind === "reply" ? 24 : 22)) &&
|
|
286
|
+
!this.#authorizedWindow(latest.state, action.kind))
|
|
287
|
+
throw new Error("实际提交时已越过运营窗口");
|
|
288
|
+
if (action.kind === "follow") {
|
|
289
|
+
const check = structuredClone(latest.state);
|
|
290
|
+
check.attempts = check.attempts.filter((a) => a.actionId !== entry.id);
|
|
291
|
+
if (check.execution.followGroup)
|
|
292
|
+
check.execution.followGroup.attempts -= 1;
|
|
293
|
+
if (this.#waitBudget(check))
|
|
294
|
+
throw new Error("实际提交前关注预算不再可用");
|
|
295
|
+
}
|
|
296
|
+
submissionMayHaveStarted = true;
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
observation = await this.#platform.findSubmission(entry);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (!submissionMayHaveStarted) {
|
|
303
|
+
const proof = await this.#store.recordProof({
|
|
304
|
+
capturedAt: this.#stamp(),
|
|
305
|
+
submissionStarted: false,
|
|
306
|
+
error: error.message.slice(0, 240),
|
|
307
|
+
});
|
|
308
|
+
const unsubmitted = structuredClone(snapshot.state), target = unsubmitted.actions.find((a) => a.id === entry.id);
|
|
309
|
+
target.status = "prepared";
|
|
310
|
+
target.preparationEvidence = proof;
|
|
311
|
+
unsubmitted.items.find((i) => i.kind === action.kind).unknown -= 1;
|
|
312
|
+
snapshot = await this.#save(snapshot, unsubmitted);
|
|
313
|
+
return {
|
|
314
|
+
snapshot,
|
|
315
|
+
need: {
|
|
316
|
+
status: "needsDecision",
|
|
317
|
+
actionId: action.id,
|
|
318
|
+
reason: "pre-submit-preparation-failed",
|
|
319
|
+
evidenceRef: proof,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
// A transport error does not distinguish a rejected write from a successful write.
|
|
324
|
+
observation = {
|
|
325
|
+
verified: false,
|
|
326
|
+
missing: ["submission-or-verification-failed"],
|
|
327
|
+
error: error.message.slice(0, 240),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
snapshot = await this.#settleProof(snapshot, entry, observation);
|
|
331
|
+
return {
|
|
332
|
+
snapshot,
|
|
333
|
+
need: observation.verified
|
|
334
|
+
? null
|
|
335
|
+
: {
|
|
336
|
+
status: "needsDecision",
|
|
337
|
+
actionId: action.id,
|
|
338
|
+
reason: "submission-unknown-do-not-resubmit",
|
|
339
|
+
accountAlert: Boolean(observation.alerts?.length),
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
export function createGrowthExecution(...args) {
|
|
345
|
+
return new GrowthExecution(...args);
|
|
346
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GrowthDependencies } from "./growth-dependencies.js";
|
|
2
|
+
export declare class ParallelRound {
|
|
3
|
+
#private;
|
|
4
|
+
parallelRound: ({ taskId, decision, selector, requestedKind, maxActions, }?: any) => Promise<any>;
|
|
5
|
+
constructor(dependencies: Pick<GrowthDependencies, "load" | "scope" | "result" | "store" | "executor" | "runtimeId" | "stamp" | "evidenceExists" | "execute" | "clock" | "releaseSession" | "session">);
|
|
6
|
+
private handleParallelRound;
|
|
7
|
+
}
|
|
8
|
+
export declare function createParallelRound(...args: ConstructorParameters<typeof ParallelRound>): ParallelRound;
|