@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
package/dist/growth.js
CHANGED
|
@@ -1,1918 +1,221 @@
|
|
|
1
|
-
import { checkRound } from "./round-state.js";
|
|
2
|
-
import { xURL } from "./platform.js";
|
|
3
1
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const reservation = before.status === "prepared" && after.status === "intent";
|
|
88
|
-
if (reservation) {
|
|
89
|
-
if (!item.actionable || item.verified + item.unknown >= item.planned)
|
|
90
|
-
throw new Error("shared-target-capacity-exhausted");
|
|
91
|
-
if (entry.kind === "follow" && waitBudget(next))
|
|
92
|
-
throw new Error("shared-follow-budget-exhausted");
|
|
93
|
-
}
|
|
94
|
-
if (after.status === "verified" &&
|
|
95
|
-
entry.kind !== "follow" &&
|
|
96
|
-
next.actions.some((a) => a.id !== entry.id &&
|
|
97
|
-
a.status === "verified" &&
|
|
98
|
-
a.publicId === after.publicId))
|
|
99
|
-
throw new Error("同一公开结果不能重复结算");
|
|
100
|
-
Object.assign(entry, after, { claim: entry.claim });
|
|
101
|
-
item.verified += newItem.verified - oldItem.verified;
|
|
102
|
-
item.unknown += newItem.unknown - oldItem.unknown;
|
|
103
|
-
if (item.unknown < 0 || item.verified + item.unknown > item.planned)
|
|
104
|
-
throw new Error("并行结算越界");
|
|
105
|
-
item.resultEvidence = [
|
|
106
|
-
...new Set([
|
|
107
|
-
...(item.resultEvidence ?? []),
|
|
108
|
-
...(newItem.resultEvidence ?? []),
|
|
109
|
-
]),
|
|
110
|
-
];
|
|
111
|
-
for (const key of [
|
|
112
|
-
"actionable",
|
|
113
|
-
"reason",
|
|
114
|
-
"resumeAt",
|
|
115
|
-
"resumeWhen",
|
|
116
|
-
"blockEvidence",
|
|
117
|
-
]) {
|
|
118
|
-
if (JSON.stringify(oldItem[key]) !== JSON.stringify(newItem[key])) {
|
|
119
|
-
if (key in newItem)
|
|
120
|
-
item[key] = newItem[key];
|
|
121
|
-
else
|
|
122
|
-
delete item[key];
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (reservation) {
|
|
126
|
-
const attempt = state.attempts.at(-1);
|
|
127
|
-
next.attempts ??= [];
|
|
128
|
-
next.attempts.push(attempt);
|
|
129
|
-
if (entry.kind === "follow") {
|
|
130
|
-
next.execution ??= {};
|
|
131
|
-
const old = next.execution.followGroup;
|
|
132
|
-
next.execution.followGroup = {
|
|
133
|
-
attempts: old?.attempts < 10 && clock() < Date.parse(old.lastAt) + 180000
|
|
134
|
-
? old.attempts + 1
|
|
135
|
-
: 1,
|
|
136
|
-
lastAt: attempt.at,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
next.status = "RUNNING";
|
|
141
|
-
next.updatedAt = stamp();
|
|
142
|
-
return next;
|
|
2
|
+
import { kinds } from "./growth-constants.js";
|
|
3
|
+
import { createGrowthPolicy } from "./growth-policy.js";
|
|
4
|
+
import { createGrowthState } from "./growth-state.js";
|
|
5
|
+
import { createGrowthExecution } from "./growth-execution.js";
|
|
6
|
+
import { createParallelRound } from "./growth-parallel.js";
|
|
7
|
+
import { createPlannedRound } from "./growth-plan.js";
|
|
8
|
+
import { createIndependentPublishing } from "./growth-publishing.js";
|
|
9
|
+
import { createGrowthReading } from "./growth-reading.js";
|
|
10
|
+
import { createGrowthRecovery } from "./growth-recovery.js";
|
|
11
|
+
import { createFollowBatch } from "./growth-batch.js";
|
|
12
|
+
import { createGrowthTasks } from "./growth-tasks.js";
|
|
13
|
+
/** Synchronous composition only; owners retain one original ledger and settlement path. */
|
|
14
|
+
export class GrowthHarness {
|
|
15
|
+
#platform;
|
|
16
|
+
#store;
|
|
17
|
+
#clock;
|
|
18
|
+
#executor;
|
|
19
|
+
#session;
|
|
20
|
+
#evidenceExists;
|
|
21
|
+
#runtimeId;
|
|
22
|
+
#authorizedWindow;
|
|
23
|
+
#scope;
|
|
24
|
+
#project;
|
|
25
|
+
#capability;
|
|
26
|
+
#waitBudget;
|
|
27
|
+
#guardInput;
|
|
28
|
+
#load;
|
|
29
|
+
#save;
|
|
30
|
+
#result;
|
|
31
|
+
#settleProof;
|
|
32
|
+
#releaseSession;
|
|
33
|
+
#execute;
|
|
34
|
+
#parallelRound;
|
|
35
|
+
#round;
|
|
36
|
+
#publishIndependent;
|
|
37
|
+
#prepareActionsForCall;
|
|
38
|
+
#readTask;
|
|
39
|
+
#createGrowthRecoveryResult;
|
|
40
|
+
#createFollowBatchResult;
|
|
41
|
+
#createGrowthTasksResult;
|
|
42
|
+
verifyPublishedPost;
|
|
43
|
+
verifySubmission;
|
|
44
|
+
verifyFollow;
|
|
45
|
+
followBatch;
|
|
46
|
+
preflight;
|
|
47
|
+
readCreator;
|
|
48
|
+
inspectAccount;
|
|
49
|
+
readPost;
|
|
50
|
+
readPerson;
|
|
51
|
+
readBookmarks;
|
|
52
|
+
searchSources;
|
|
53
|
+
readFeedback;
|
|
54
|
+
runGrowthRound;
|
|
55
|
+
resumeGrowthRound;
|
|
56
|
+
inspectRound;
|
|
57
|
+
followOne;
|
|
58
|
+
publishPost;
|
|
59
|
+
replyToPost;
|
|
60
|
+
constructor({ platform, store, clock = () => Date.now(), executor, } = {}) {
|
|
61
|
+
this.#platform = platform;
|
|
62
|
+
this.#store = store;
|
|
63
|
+
this.#clock = clock;
|
|
64
|
+
this.#executor = executor;
|
|
65
|
+
this.#session = { claim: undefined, running: false };
|
|
66
|
+
this.#evidenceExists = this.#store.evidenceExists ?? (() => false);
|
|
67
|
+
this.#runtimeId = randomUUID();
|
|
68
|
+
if (this.#executor &&
|
|
69
|
+
(!this.#executor.id ||
|
|
70
|
+
!this.#executor.tabId ||
|
|
71
|
+
!Array.isArray(this.#executor.scopeKinds) ||
|
|
72
|
+
!this.#executor.scopeKinds.length ||
|
|
73
|
+
new Set(this.#executor.scopeKinds).size !==
|
|
74
|
+
this.#executor.scopeKinds.length ||
|
|
75
|
+
this.#executor.scopeKinds.some((k) => !kinds.includes(k)) ||
|
|
76
|
+
!this.#evidenceExists(this.#executor.evidenceRef) ||
|
|
77
|
+
typeof this.#store.transaction !== "function"))
|
|
78
|
+
throw new Error("并行执行者需ID、独立tab、有效职责及原合同证据与transaction");
|
|
79
|
+
const { authorizedWindow, scope, project, capability, waitBudget, guardInput, } = createGrowthPolicy({
|
|
80
|
+
executor: this.#executor,
|
|
81
|
+
clock: this.#clock,
|
|
82
|
+
evidenceExists: this.#evidenceExists,
|
|
83
|
+
platform: this.#platform,
|
|
84
|
+
store: this.#store,
|
|
143
85
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
86
|
+
this.#authorizedWindow = authorizedWindow;
|
|
87
|
+
this.#scope = scope;
|
|
88
|
+
this.#project = project;
|
|
89
|
+
this.#capability = capability;
|
|
90
|
+
this.#waitBudget = waitBudget;
|
|
91
|
+
this.#guardInput = guardInput;
|
|
92
|
+
const { load, save, result, settleProof, releaseSession } = createGrowthState({
|
|
93
|
+
store: this.#store,
|
|
94
|
+
project: this.#project,
|
|
95
|
+
clock: this.#clock,
|
|
96
|
+
evidenceExists: this.#evidenceExists,
|
|
97
|
+
stamp: this.stamp.bind(this),
|
|
98
|
+
executor: this.#executor,
|
|
99
|
+
waitBudget: this.#waitBudget,
|
|
100
|
+
platform: this.#platform,
|
|
101
|
+
runtimeId: this.#runtimeId,
|
|
102
|
+
session: this.#session,
|
|
149
103
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
...(gate.errors.length ? { settlementErrors: gate.errors } : {}),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function capability(operation, input = {}) {
|
|
172
|
-
const kind = input.kind ??
|
|
173
|
-
{
|
|
174
|
-
followOne: "follow",
|
|
175
|
-
followBatch: "follow",
|
|
176
|
-
submitFollow: "follow",
|
|
177
|
-
publishPost: "original",
|
|
178
|
-
submitPost: "original",
|
|
179
|
-
replyToPost: "reply",
|
|
180
|
-
submitReply: "reply",
|
|
181
|
-
}[operation];
|
|
182
|
-
const required = [
|
|
183
|
-
"preflight",
|
|
184
|
-
"readAccount",
|
|
185
|
-
"findSubmission",
|
|
186
|
-
"captureProof",
|
|
187
|
-
...(operation === "read" ? [] : [operations[kind]]),
|
|
188
|
-
];
|
|
189
|
-
if (kind === "follow")
|
|
190
|
-
required.push("readPerson");
|
|
191
|
-
const missing = required.filter((name) => typeof platform?.[name] !== "function");
|
|
192
|
-
const storage = ["load", "save", "withLock", "recordProof"].filter((name) => typeof store?.[name] !== "function");
|
|
193
|
-
if (operation === "followBatch" && typeof store?.transaction !== "function")
|
|
194
|
-
storage.push("transaction");
|
|
195
|
-
if (kind === "original" &&
|
|
196
|
-
operation !== "read" &&
|
|
197
|
-
typeof store?.fingerprint !== "function")
|
|
198
|
-
storage.push("fingerprint");
|
|
199
|
-
const lower = typeof platform?.preflight === "function"
|
|
200
|
-
? platform.preflight({
|
|
201
|
-
operation: operation === "read" ? "read" : operations[kind],
|
|
202
|
-
input,
|
|
203
|
-
})
|
|
204
|
-
: { covered: false, missing: [] };
|
|
205
|
-
return {
|
|
206
|
-
covered: !missing.length && !storage.length && lower.covered,
|
|
207
|
-
missing: [
|
|
208
|
-
...missing.map((n) => `platform.${n}`),
|
|
209
|
-
...storage.map((n) => `store.${n}`),
|
|
210
|
-
...(lower.missing ?? []),
|
|
211
|
-
],
|
|
212
|
-
owner: missing.length
|
|
213
|
-
? "scripts/x-harness-platform.mjs"
|
|
214
|
-
: storage.length
|
|
215
|
-
? "scripts/x-harness-store.mjs"
|
|
216
|
-
: lower.owner,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
function waitBudget(state) {
|
|
220
|
-
const now = clock();
|
|
221
|
-
const hour = Number(new Intl.DateTimeFormat("en-GB", {
|
|
222
|
-
timeZone: "Asia/Shanghai",
|
|
223
|
-
hour: "2-digit",
|
|
224
|
-
hourCycle: "h23",
|
|
225
|
-
}).format(now));
|
|
226
|
-
if (hour < 7 || hour >= 22)
|
|
227
|
-
return {
|
|
228
|
-
status: "needsDecision",
|
|
229
|
-
reason: "outside-contract-write-window",
|
|
230
|
-
};
|
|
231
|
-
const coverage = state.attemptsCoverage;
|
|
232
|
-
if (!coverage?.complete ||
|
|
233
|
-
!Number.isFinite(Date.parse(coverage.since)) ||
|
|
234
|
-
Date.parse(coverage.since) > now - 3600000 ||
|
|
235
|
-
!evidenceExists(coverage.evidenceRef))
|
|
236
|
-
return {
|
|
237
|
-
status: "needsDecision",
|
|
238
|
-
reason: "shared-attempt-history-incomplete",
|
|
239
|
-
};
|
|
240
|
-
const attempts = state.attempts ?? [];
|
|
241
|
-
if (attempts.some((a) => !kinds.includes(a.kind) || !Number.isFinite(Date.parse(a.at))))
|
|
242
|
-
return { status: "needsDecision", reason: "attempt-history-invalid" };
|
|
243
|
-
const follows = attempts.filter((a) => a.kind === "follow");
|
|
244
|
-
for (const [duration, limit] of followWindows) {
|
|
245
|
-
const recent = follows
|
|
246
|
-
.filter((a) => Date.parse(a.at) > now - duration)
|
|
247
|
-
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
248
|
-
if (recent.length >= limit)
|
|
249
|
-
return {
|
|
250
|
-
status: "waiting",
|
|
251
|
-
reason: "follow-rolling-budget",
|
|
252
|
-
resumeAt: new Date(Date.parse(recent[recent.length - limit].at) + duration).toISOString(),
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
const group = state.execution?.followGroup;
|
|
256
|
-
if (group?.attempts >= 10 && now < Date.parse(group.lastAt) + 180000)
|
|
257
|
-
return {
|
|
258
|
-
status: "waiting",
|
|
259
|
-
reason: "follow-group-cooldown",
|
|
260
|
-
resumeAt: new Date(Date.parse(group.lastAt) + 180000).toISOString(),
|
|
261
|
-
};
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
function guardInput(state, action, independentExecution = undefined) {
|
|
265
|
-
if (!independentExecution && !scope(state).includes(action.kind))
|
|
266
|
-
return "kind-outside-round-scope";
|
|
267
|
-
if (independentExecution) {
|
|
268
|
-
if (action.kind !== "original" ||
|
|
269
|
-
!independentExecution.tabId ||
|
|
270
|
-
!evidenceExists(independentExecution.evidenceRef) ||
|
|
271
|
-
!evidenceExists(independentExecution.scopeEvidenceRef))
|
|
272
|
-
return "independent-execution-proof-missing";
|
|
273
|
-
}
|
|
274
|
-
else if (!executor &&
|
|
275
|
-
(!state.execution?.exclusive || state.execution.owner !== "harness"))
|
|
276
|
-
return "exclusive-account-execution-unconfirmed";
|
|
277
|
-
if (!same(state.accountHandle, action.accountHandle))
|
|
278
|
-
return "account-identity-mismatch";
|
|
279
|
-
if (!evidenceExists(action.accountEvidenceRef) ||
|
|
280
|
-
xURL(action.accountURL).pathname.toLowerCase() !==
|
|
281
|
-
`/${state.accountHandle}`.toLowerCase())
|
|
282
|
-
return "own-profile-proof-missing";
|
|
283
|
-
if (!action.quality?.approved ||
|
|
284
|
-
!evidenceExists(action.quality.evidenceRef))
|
|
285
|
-
return "quality-review-required";
|
|
286
|
-
if (action.kind === "follow") {
|
|
287
|
-
if (!same(action.quality.handle, action.handle) ||
|
|
288
|
-
action.quality.blue !== true ||
|
|
289
|
-
action.quality.chinese !== true)
|
|
290
|
-
return "follow-qualification-required";
|
|
291
|
-
if (!evidenceExists(action.evidenceRef))
|
|
292
|
-
return "person-observation-proof-missing";
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
if (!action.text || action.quality.approvedText !== action.text)
|
|
296
|
-
return "exact-text-quality-review-required";
|
|
297
|
-
if (action.kind === "original" &&
|
|
298
|
-
(!action.draftPath || !evidenceExists(action.draftPath)))
|
|
299
|
-
return "draft-source-missing";
|
|
300
|
-
if (action.kind === "original" &&
|
|
301
|
-
(!action.draftPath.endsWith(".post.md") ||
|
|
302
|
-
action.quality.draftFingerprint !==
|
|
303
|
-
store.fingerprint(action.draftPath)))
|
|
304
|
-
return "draft-changed-since-quality-review";
|
|
305
|
-
if (action.kind === "original" &&
|
|
306
|
-
(!evidenceExists(action.composeEvidenceRef) ||
|
|
307
|
-
xURL(action.composeURL).pathname !== "/compose/post"))
|
|
308
|
-
return "observed-composer-proof-missing";
|
|
309
|
-
if (action.kind === "reply" &&
|
|
310
|
-
(!/^\d+$/.test(action.postId ?? "") ||
|
|
311
|
-
!evidenceExists(action.evidenceRef)))
|
|
312
|
-
return "reply-target-proof-missing";
|
|
313
|
-
}
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
async function settleProof(snapshot, action, observation, { preserveStatus = false } = {}) {
|
|
317
|
-
const proof = await store.recordProof(await platform.captureProof({ observation }));
|
|
318
|
-
const state = structuredClone(snapshot.state);
|
|
319
|
-
const entry = state.actions.find((a) => a.id === action.id);
|
|
320
|
-
const item = state.items.find((i) => i.kind === action.kind);
|
|
321
|
-
if (observation.verified) {
|
|
322
|
-
if (action.kind !== "follow" &&
|
|
323
|
-
(!/^\d+$/.test(observation.publicId ?? "") ||
|
|
324
|
-
state.actions.some((a) => a.id !== action.id &&
|
|
325
|
-
a.status === "verified" &&
|
|
326
|
-
a.publicId === observation.publicId)))
|
|
327
|
-
throw new Error("同一公开结果不能重复结算");
|
|
328
|
-
if (["intent", "unknown"].includes(entry.status))
|
|
329
|
-
item.unknown -= 1;
|
|
330
|
-
if (entry.status !== "verified")
|
|
331
|
-
item.verified += 1;
|
|
332
|
-
entry.status = "verified";
|
|
333
|
-
entry.publicId = observation.publicId;
|
|
334
|
-
entry.resultEvidence = proof;
|
|
335
|
-
item.resultEvidence = [
|
|
336
|
-
...new Set([...(item.resultEvidence ?? []), proof]),
|
|
337
|
-
];
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
if (!["intent", "unknown"].includes(entry.status))
|
|
341
|
-
item.unknown += 1;
|
|
342
|
-
entry.status = "unknown";
|
|
343
|
-
entry.resultEvidence = proof;
|
|
344
|
-
}
|
|
345
|
-
if (item.verified > item.planned || item.unknown < 0)
|
|
346
|
-
throw new Error("结算越界:不覆盖原目标或重复计数");
|
|
347
|
-
if (!preserveStatus)
|
|
348
|
-
state.status = "RUNNING";
|
|
349
|
-
return save(snapshot, state);
|
|
350
|
-
}
|
|
351
|
-
async function execute(snapshot, action) {
|
|
352
|
-
const recovering = ["intent", "unknown"].includes(action.status);
|
|
353
|
-
const caps = capability(recovering ? "read" : operations[action.kind], action);
|
|
354
|
-
if (!caps.covered)
|
|
355
|
-
return {
|
|
356
|
-
snapshot,
|
|
357
|
-
need: { status: "needsCapability", actionId: action.id, ...caps },
|
|
358
|
-
};
|
|
359
|
-
// Recovery is read-only even outside the write window. Never retry an intent.
|
|
360
|
-
if (recovering) {
|
|
361
|
-
let observation;
|
|
362
|
-
try {
|
|
363
|
-
observation = await platform.findSubmission(action);
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
observation = {
|
|
367
|
-
verified: false,
|
|
368
|
-
missing: ["dedup-read-failed"],
|
|
369
|
-
error: error.message.slice(0, 240),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
snapshot = await settleProof(snapshot, action, observation);
|
|
373
|
-
return {
|
|
374
|
-
snapshot,
|
|
375
|
-
need: observation.verified
|
|
376
|
-
? null
|
|
377
|
-
: {
|
|
378
|
-
status: "needsDecision",
|
|
379
|
-
actionId: action.id,
|
|
380
|
-
reason: "submission-unknown-do-not-resubmit",
|
|
381
|
-
accountAlert: Boolean(observation.alerts?.length),
|
|
382
|
-
},
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
if (snapshot.state.items
|
|
386
|
-
.find((i) => i.kind === action.kind)
|
|
387
|
-
.reason?.startsWith("harness:") &&
|
|
388
|
-
Date.parse(snapshot.state.items.find((i) => i.kind === action.kind).resumeAt) <= clock()) {
|
|
389
|
-
const next = structuredClone(snapshot.state), pending = next.items.find((i) => i.kind === action.kind);
|
|
390
|
-
pending.actionable = true;
|
|
391
|
-
delete pending.reason;
|
|
392
|
-
delete pending.resumeAt;
|
|
393
|
-
delete pending.resumeWhen;
|
|
394
|
-
delete pending.blockEvidence;
|
|
395
|
-
next.status = "RUNNING";
|
|
396
|
-
snapshot = await save(snapshot, next);
|
|
397
|
-
}
|
|
398
|
-
const state = snapshot.state;
|
|
399
|
-
const item = state.items.find((i) => i.kind === action.kind);
|
|
400
|
-
if (!item.actionable)
|
|
401
|
-
return {
|
|
402
|
-
snapshot,
|
|
403
|
-
need: {
|
|
404
|
-
status: "needsDecision",
|
|
405
|
-
actionId: action.id,
|
|
406
|
-
reason: item.reason ?? "original-kind-not-actionable",
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
const inputError = guardInput(state, action);
|
|
410
|
-
if (inputError)
|
|
411
|
-
return {
|
|
412
|
-
snapshot,
|
|
413
|
-
need: {
|
|
414
|
-
status: "needsDecision",
|
|
415
|
-
actionId: action.id,
|
|
416
|
-
reason: inputError,
|
|
417
|
-
},
|
|
418
|
-
};
|
|
419
|
-
let own;
|
|
420
|
-
try {
|
|
421
|
-
own = await platform.readAccount();
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
return {
|
|
425
|
-
snapshot,
|
|
426
|
-
need: {
|
|
427
|
-
status: "needsDecision",
|
|
428
|
-
actionId: action.id,
|
|
429
|
-
reason: "account-read-failed",
|
|
430
|
-
error: error.message.slice(0, 240),
|
|
431
|
-
},
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
if (own.needsStateCheck ||
|
|
435
|
-
own.alerts?.length ||
|
|
436
|
-
!same(own.accountHandle, state.accountHandle))
|
|
437
|
-
return {
|
|
438
|
-
snapshot,
|
|
439
|
-
need: {
|
|
440
|
-
status: "needsDecision",
|
|
441
|
-
actionId: action.id,
|
|
442
|
-
reason: "fresh-account-unconfirmed",
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
if (action.kind === "follow") {
|
|
446
|
-
const budget = waitBudget(state);
|
|
447
|
-
if (budget)
|
|
448
|
-
return { snapshot, need: { ...budget, actionId: action.id } };
|
|
449
|
-
let value;
|
|
450
|
-
try {
|
|
451
|
-
value = await platform.readPerson(action);
|
|
452
|
-
}
|
|
453
|
-
catch (error) {
|
|
454
|
-
return {
|
|
455
|
-
snapshot,
|
|
456
|
-
need: {
|
|
457
|
-
status: "needsDecision",
|
|
458
|
-
actionId: action.id,
|
|
459
|
-
reason: "person-read-failed",
|
|
460
|
-
error: error.message.slice(0, 240),
|
|
461
|
-
},
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
if (value.needsStateCheck ||
|
|
465
|
-
value.alerts?.length ||
|
|
466
|
-
value.relationship === "unknown" ||
|
|
467
|
-
!same(value.accountHandle, state.accountHandle))
|
|
468
|
-
return {
|
|
469
|
-
snapshot,
|
|
470
|
-
need: {
|
|
471
|
-
status: "needsDecision",
|
|
472
|
-
actionId: action.id,
|
|
473
|
-
reason: "fresh-person-unconfirmed",
|
|
474
|
-
},
|
|
475
|
-
};
|
|
476
|
-
if (value.blue === false)
|
|
477
|
-
return {
|
|
478
|
-
snapshot,
|
|
479
|
-
need: {
|
|
480
|
-
status: "needsDecision",
|
|
481
|
-
actionId: action.id,
|
|
482
|
-
reason: "fresh-blue-qualification-contradicts-review",
|
|
483
|
-
},
|
|
484
|
-
};
|
|
485
|
-
if (value.relationship === "following") {
|
|
486
|
-
const next = structuredClone(state);
|
|
487
|
-
next.actions.find((a) => a.id === action.id).status =
|
|
488
|
-
"preexisting";
|
|
489
|
-
return { snapshot: await save(snapshot, next), need: null };
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
const hour = Number(new Intl.DateTimeFormat("en-GB", {
|
|
494
|
-
timeZone: "Asia/Shanghai",
|
|
495
|
-
hour: "2-digit",
|
|
496
|
-
hourCycle: "h23",
|
|
497
|
-
}).format(clock()));
|
|
498
|
-
if (hour < 7 || hour >= 22)
|
|
499
|
-
return {
|
|
500
|
-
snapshot,
|
|
501
|
-
need: {
|
|
502
|
-
status: "needsDecision",
|
|
503
|
-
actionId: action.id,
|
|
504
|
-
reason: "outside-contract-write-window",
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
let beforeIds = [];
|
|
509
|
-
if (action.kind !== "follow") {
|
|
510
|
-
let baseline;
|
|
511
|
-
try {
|
|
512
|
-
baseline = await platform.findSubmission({
|
|
513
|
-
...action,
|
|
514
|
-
intentAt: stamp(),
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
catch (error) {
|
|
518
|
-
return {
|
|
519
|
-
snapshot,
|
|
520
|
-
need: {
|
|
521
|
-
status: "needsDecision",
|
|
522
|
-
actionId: action.id,
|
|
523
|
-
reason: "publication-baseline-read-failed",
|
|
524
|
-
error: error.message.slice(0, 240),
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
if (baseline.needsStateCheck || baseline.alerts?.length)
|
|
529
|
-
return {
|
|
530
|
-
snapshot,
|
|
531
|
-
need: {
|
|
532
|
-
status: "needsDecision",
|
|
533
|
-
actionId: action.id,
|
|
534
|
-
reason: "publication-baseline-unconfirmed",
|
|
535
|
-
},
|
|
536
|
-
};
|
|
537
|
-
beforeIds = (baseline.posts ?? []).map((p) => p.id).filter(Boolean);
|
|
538
|
-
}
|
|
539
|
-
// Persist intent BEFORE any externally visible submission. Even a failed attempt consumes budget.
|
|
540
|
-
const next = structuredClone(state);
|
|
541
|
-
const entry = next.actions.find((a) => a.id === action.id);
|
|
542
|
-
entry.status = "intent";
|
|
543
|
-
entry.intentAt = stamp();
|
|
544
|
-
entry.beforeIds = beforeIds;
|
|
545
|
-
next.items.find((i) => i.kind === action.kind).unknown += 1;
|
|
546
|
-
next.attempts ??= [];
|
|
547
|
-
next.attempts.push({
|
|
548
|
-
actionId: action.id,
|
|
549
|
-
kind: action.kind,
|
|
550
|
-
at: entry.intentAt,
|
|
104
|
+
this.#load = load;
|
|
105
|
+
this.#save = save;
|
|
106
|
+
this.#result = result;
|
|
107
|
+
this.#settleProof = settleProof;
|
|
108
|
+
this.#releaseSession = releaseSession;
|
|
109
|
+
const { execute } = createGrowthExecution({
|
|
110
|
+
capability: this.#capability,
|
|
111
|
+
platform: this.#platform,
|
|
112
|
+
settleProof: this.#settleProof,
|
|
113
|
+
clock: this.#clock,
|
|
114
|
+
save: this.#save,
|
|
115
|
+
guardInput: this.#guardInput,
|
|
116
|
+
waitBudget: this.#waitBudget,
|
|
117
|
+
authorizedWindow: this.#authorizedWindow,
|
|
118
|
+
stamp: this.stamp.bind(this),
|
|
119
|
+
store: this.#store,
|
|
120
|
+
executor: this.#executor,
|
|
121
|
+
session: this.#session,
|
|
551
122
|
});
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
...entry,
|
|
567
|
-
beforeSubmit: async () => {
|
|
568
|
-
const latest = await store.load();
|
|
569
|
-
if (executor) {
|
|
570
|
-
const current = latest.state.actions.find((a) => a.id === entry.id);
|
|
571
|
-
if (latest.state.batchId !== snapshot.state.batchId ||
|
|
572
|
-
current?.claim?.token !== claim.token ||
|
|
573
|
-
JSON.stringify(current) !==
|
|
574
|
-
JSON.stringify(snapshot.state.actions.find((a) => a.id === entry.id)))
|
|
575
|
-
throw new Error("提交前本动作或claim变化");
|
|
576
|
-
}
|
|
577
|
-
else if (latest.revision !== snapshot.revision)
|
|
578
|
-
throw new Error("提交前原台账变化,保留intent并重核执行权");
|
|
579
|
-
const inputError = guardInput(latest.state, latest.state.actions.find((a) => a.id === entry.id));
|
|
580
|
-
if (inputError)
|
|
581
|
-
throw new Error(inputError);
|
|
582
|
-
const hour = Number(new Intl.DateTimeFormat("en-GB", {
|
|
583
|
-
timeZone: "Asia/Shanghai",
|
|
584
|
-
hour: "2-digit",
|
|
585
|
-
hourCycle: "h23",
|
|
586
|
-
}).format(clock()));
|
|
587
|
-
if (hour < 7 || hour >= 22)
|
|
588
|
-
throw new Error("实际提交时已越过运营窗口");
|
|
589
|
-
if (action.kind === "follow") {
|
|
590
|
-
const check = structuredClone(latest.state);
|
|
591
|
-
check.attempts = check.attempts.filter((a) => a.actionId !== entry.id);
|
|
592
|
-
if (check.execution.followGroup)
|
|
593
|
-
check.execution.followGroup.attempts -= 1;
|
|
594
|
-
if (waitBudget(check))
|
|
595
|
-
throw new Error("实际提交前关注预算不再可用");
|
|
596
|
-
}
|
|
597
|
-
submissionMayHaveStarted = true;
|
|
598
|
-
},
|
|
599
|
-
});
|
|
600
|
-
observation = await platform.findSubmission(entry);
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
if (!submissionMayHaveStarted) {
|
|
604
|
-
const proof = await store.recordProof({
|
|
605
|
-
capturedAt: stamp(),
|
|
606
|
-
submissionStarted: false,
|
|
607
|
-
error: error.message.slice(0, 240),
|
|
608
|
-
});
|
|
609
|
-
const unsubmitted = structuredClone(snapshot.state), target = unsubmitted.actions.find((a) => a.id === entry.id);
|
|
610
|
-
target.status = "prepared";
|
|
611
|
-
target.preparationEvidence = proof;
|
|
612
|
-
unsubmitted.items.find((i) => i.kind === action.kind).unknown -= 1;
|
|
613
|
-
snapshot = await save(snapshot, unsubmitted);
|
|
614
|
-
return {
|
|
615
|
-
snapshot,
|
|
616
|
-
need: {
|
|
617
|
-
status: "needsDecision",
|
|
618
|
-
actionId: action.id,
|
|
619
|
-
reason: "pre-submit-preparation-failed",
|
|
620
|
-
evidenceRef: proof,
|
|
621
|
-
},
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
// A transport error does not distinguish a rejected write from a successful write.
|
|
625
|
-
observation = {
|
|
626
|
-
verified: false,
|
|
627
|
-
missing: ["submission-or-verification-failed"],
|
|
628
|
-
error: error.message.slice(0, 240),
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
snapshot = await settleProof(snapshot, entry, observation);
|
|
632
|
-
return {
|
|
633
|
-
snapshot,
|
|
634
|
-
need: observation.verified
|
|
635
|
-
? null
|
|
636
|
-
: {
|
|
637
|
-
status: "needsDecision",
|
|
638
|
-
actionId: action.id,
|
|
639
|
-
reason: "submission-unknown-do-not-resubmit",
|
|
640
|
-
accountAlert: Boolean(observation.alerts?.length),
|
|
641
|
-
},
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
async function parallelRound({ taskId, decision, selector, requestedKind, maxActions = 10, } = {}) {
|
|
645
|
-
if (running)
|
|
646
|
-
return {
|
|
647
|
-
status: "needsDecision",
|
|
648
|
-
mayEnd: false,
|
|
649
|
-
taskId,
|
|
650
|
-
decision: [{ reason: "same-tab-operation-running" }],
|
|
651
|
-
};
|
|
652
|
-
running = true;
|
|
653
|
-
let completed = 0;
|
|
654
|
-
const needs = [];
|
|
655
|
-
try {
|
|
656
|
-
if (!Number.isInteger(maxActions) || maxActions < 1 || maxActions > 100)
|
|
657
|
-
throw new Error("maxActions须为1–100");
|
|
658
|
-
let snapshot = await load(taskId);
|
|
659
|
-
const activeKinds = scope(snapshot.state);
|
|
660
|
-
if (requestedKind && !activeKinds.includes(requestedKind))
|
|
661
|
-
return {
|
|
662
|
-
...result(snapshot),
|
|
663
|
-
status: "needsDecision",
|
|
664
|
-
decision: [{ reason: "kind-outside-round-scope" }],
|
|
665
|
-
};
|
|
666
|
-
snapshot = await store.transaction((latest) => {
|
|
667
|
-
if (latest.state.batchId !== taskId)
|
|
668
|
-
throw new Error("original-task-changed");
|
|
669
|
-
const next = structuredClone(latest.state);
|
|
670
|
-
next.executions ??= {};
|
|
671
|
-
if (Object.values(next.executions).some((e) => (e.tabId === executor.tabId || e.id === executor.id) &&
|
|
672
|
-
e.runtimeId !== runtimeId &&
|
|
673
|
-
(e.activity || e.actionId) &&
|
|
674
|
-
store.ownerAlive(e.pid)))
|
|
675
|
-
throw new Error("same-tab-or-executor-claimed");
|
|
676
|
-
next.executions[executor.id] = {
|
|
677
|
-
...next.executions[executor.id],
|
|
678
|
-
...executor,
|
|
679
|
-
runtimeId,
|
|
680
|
-
pid: store.processId,
|
|
681
|
-
activity: "round",
|
|
682
|
-
status: "RUNNING",
|
|
683
|
-
updatedAt: stamp(),
|
|
684
|
-
};
|
|
685
|
-
if (decision?.type === "prepareActions") {
|
|
686
|
-
if (!Array.isArray(decision.actions) ||
|
|
687
|
-
!same(next.accountHandle, decision.accountHandle) ||
|
|
688
|
-
!evidenceExists(decision.ownershipEvidenceRef))
|
|
689
|
-
throw new Error("需原账号核验和完整动作队列");
|
|
690
|
-
next.actions ??= [];
|
|
691
|
-
for (const action of decision.actions) {
|
|
692
|
-
if (!action.id ||
|
|
693
|
-
!activeKinds.includes(action.kind) ||
|
|
694
|
-
action.status !== "prepared" ||
|
|
695
|
-
next.actions.some((a) => a.id === action.id))
|
|
696
|
-
throw new Error("原prepared动作ID无效");
|
|
697
|
-
next.actions.push(structuredClone(action));
|
|
698
|
-
}
|
|
699
|
-
if (next.items.some((i) => next.actions.filter((a) => a.kind === i.kind && a.status === "prepared").length >
|
|
700
|
-
i.planned - i.verified - i.unknown))
|
|
701
|
-
throw new Error("队列超过原目标");
|
|
702
|
-
if (decision.sharedAttempts) {
|
|
703
|
-
if (!Array.isArray(decision.sharedAttempts))
|
|
704
|
-
throw new Error("共享尝试须为数组");
|
|
705
|
-
next.attempts = [
|
|
706
|
-
...(next.attempts ?? []),
|
|
707
|
-
...structuredClone(decision.sharedAttempts),
|
|
708
|
-
];
|
|
709
|
-
}
|
|
710
|
-
if (decision.attemptsCoverage) {
|
|
711
|
-
if (!decision.attemptsCoverage.complete ||
|
|
712
|
-
!evidenceExists(decision.attemptsCoverage.evidenceRef))
|
|
713
|
-
throw new Error("共享历史缺证据");
|
|
714
|
-
next.attemptsCoverage = structuredClone(decision.attemptsCoverage);
|
|
715
|
-
}
|
|
716
|
-
if (decision.nextScheduledCheckAt)
|
|
717
|
-
next.executions[executor.id].nextScheduledCheckAt =
|
|
718
|
-
decision.nextScheduledCheckAt;
|
|
719
|
-
}
|
|
720
|
-
else if (decision) {
|
|
721
|
-
const action = next.actions?.find((a) => a.id === decision.actionId);
|
|
722
|
-
if (!action ||
|
|
723
|
-
!activeKinds.includes(action.kind) ||
|
|
724
|
-
action.status !== "prepared" ||
|
|
725
|
-
action.claim ||
|
|
726
|
-
typeof decision.approved !== "boolean" ||
|
|
727
|
-
!evidenceExists(decision.evidenceRef))
|
|
728
|
-
throw new Error("审查只绑定职责内未领取的原prepared动作");
|
|
729
|
-
if (!decision.approved)
|
|
730
|
-
action.status = "skipped";
|
|
731
|
-
else
|
|
732
|
-
action.quality = {
|
|
733
|
-
approved: true,
|
|
734
|
-
evidenceRef: decision.evidenceRef,
|
|
735
|
-
...(action.kind === "follow"
|
|
736
|
-
? {
|
|
737
|
-
handle: action.handle,
|
|
738
|
-
blue: decision.blue === true,
|
|
739
|
-
chinese: decision.chinese === true,
|
|
740
|
-
}
|
|
741
|
-
: {
|
|
742
|
-
approvedText: decision.approvedText,
|
|
743
|
-
...(action.kind === "original"
|
|
744
|
-
? { draftFingerprint: decision.draftFingerprint }
|
|
745
|
-
: {}),
|
|
746
|
-
}),
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
next.updatedAt = stamp();
|
|
750
|
-
return next;
|
|
751
|
-
});
|
|
752
|
-
for (const kind of activeKinds) {
|
|
753
|
-
const queue = (snapshot.state.actions ?? [])
|
|
754
|
-
.filter((a) => a.kind === kind && (!selector || selector(a)))
|
|
755
|
-
.sort((a, b) => Number(["intent", "unknown"].includes(b.status)) -
|
|
756
|
-
Number(["intent", "unknown"].includes(a.status)));
|
|
757
|
-
if (snapshot.state.items.find((i) => i.kind === kind).unknown >
|
|
758
|
-
snapshot.state.actions.filter((a) => a.kind === kind && ["intent", "unknown"].includes(a.status)).length) {
|
|
759
|
-
needs.push({
|
|
760
|
-
status: "needsDecision",
|
|
761
|
-
reason: "original-unmapped-unknown-needs-dedup",
|
|
762
|
-
kind,
|
|
763
|
-
});
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
for (const original of queue) {
|
|
767
|
-
if (completed >= maxActions)
|
|
768
|
-
break;
|
|
769
|
-
if (["verified", "preexisting", "skipped"].includes(original.status))
|
|
770
|
-
continue;
|
|
771
|
-
try {
|
|
772
|
-
snapshot = await store.transaction((latest) => {
|
|
773
|
-
const next = structuredClone(latest.state), action = next.actions.find((a) => a.id === original.id);
|
|
774
|
-
if (next.batchId !== taskId)
|
|
775
|
-
throw new Error("original-task-changed");
|
|
776
|
-
if (["verified", "preexisting", "skipped"].includes(action.status))
|
|
777
|
-
return null;
|
|
778
|
-
const sameTarget = (other) => other.kind === action.kind &&
|
|
779
|
-
(kind === "follow"
|
|
780
|
-
? same(other.handle, action.handle)
|
|
781
|
-
: kind === "original"
|
|
782
|
-
? other.draftPath === action.draftPath
|
|
783
|
-
: other.postId === action.postId &&
|
|
784
|
-
other.text === action.text);
|
|
785
|
-
if (next.actions.some((a) => a.id !== action.id &&
|
|
786
|
-
sameTarget(a) &&
|
|
787
|
-
(a.claim ||
|
|
788
|
-
["intent", "unknown", "verified"].includes(a.status))))
|
|
789
|
-
throw new Error("same-target-already-claimed-or-submitted");
|
|
790
|
-
if (action.claim && store.ownerAlive(action.claim.pid))
|
|
791
|
-
throw new Error("same-action-claimed");
|
|
792
|
-
if (Object.values(next.executions).some((e) => e.tabId === executor.tabId &&
|
|
793
|
-
e.actionId &&
|
|
794
|
-
e.runtimeId !== runtimeId &&
|
|
795
|
-
store.ownerAlive(e.pid)))
|
|
796
|
-
throw new Error("same-tab-claimed");
|
|
797
|
-
claim = {
|
|
798
|
-
actionId: action.id,
|
|
799
|
-
token: randomUUID(),
|
|
800
|
-
executorId: executor.id,
|
|
801
|
-
runtimeId,
|
|
802
|
-
pid: store.processId,
|
|
803
|
-
acquiredAt: stamp(),
|
|
804
|
-
};
|
|
805
|
-
action.claim = claim;
|
|
806
|
-
next.executions[executor.id].actionId = action.id;
|
|
807
|
-
next.updatedAt = stamp();
|
|
808
|
-
return next;
|
|
809
|
-
});
|
|
810
|
-
if (!claim)
|
|
811
|
-
continue;
|
|
812
|
-
const outcome = await execute(snapshot, snapshot.state.actions.find((a) => a.id === original.id));
|
|
813
|
-
snapshot = outcome.snapshot;
|
|
814
|
-
completed++;
|
|
815
|
-
if (outcome.need) {
|
|
816
|
-
needs.push(outcome.need);
|
|
817
|
-
break;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
catch (error) {
|
|
821
|
-
needs.push({
|
|
822
|
-
status: "needsDecision",
|
|
823
|
-
kind,
|
|
824
|
-
actionId: original.id,
|
|
825
|
-
reason: /claimed|same-target/.test(error.message)
|
|
826
|
-
? "action-or-tab-claimed"
|
|
827
|
-
: /budget|capacity/.test(error.message)
|
|
828
|
-
? "shared-reservation-unavailable"
|
|
829
|
-
: "harness-boundary-error",
|
|
830
|
-
error: error.message.slice(0, 240),
|
|
831
|
-
});
|
|
832
|
-
break;
|
|
833
|
-
}
|
|
834
|
-
finally {
|
|
835
|
-
if (claim) {
|
|
836
|
-
const owned = claim;
|
|
837
|
-
claim = null;
|
|
838
|
-
snapshot = await store.transaction((latest) => {
|
|
839
|
-
const next = structuredClone(latest.state), action = next.actions?.find((a) => a.id === owned.actionId);
|
|
840
|
-
if (action?.claim?.token !== owned.token)
|
|
841
|
-
return null;
|
|
842
|
-
delete action.claim;
|
|
843
|
-
if (next.executions[executor.id]?.runtimeId === runtimeId)
|
|
844
|
-
delete next.executions[executor.id].actionId;
|
|
845
|
-
next.updatedAt = stamp();
|
|
846
|
-
return next;
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
snapshot = await store.transaction(async (latest) => {
|
|
853
|
-
const next = structuredClone(latest.state), own = next.executions[executor.id];
|
|
854
|
-
const pending = next.items.filter((i) => activeKinds.includes(i.kind) &&
|
|
855
|
-
(i.verified < i.planned || i.unknown));
|
|
856
|
-
own.status = pending.length ? "RUNNING" : "COMPLETE";
|
|
857
|
-
const scheduled = own.nextScheduledCheckAt ??
|
|
858
|
-
next.execution?.nextScheduledCheckAt ??
|
|
859
|
-
next.stop?.nextScheduledCheckAt;
|
|
860
|
-
const waits = pending.map((i) => needs.find((n) => n.status === "waiting" &&
|
|
861
|
-
next.actions.find((a) => a.id === n.actionId)?.kind ===
|
|
862
|
-
i.kind));
|
|
863
|
-
if (pending.length &&
|
|
864
|
-
pending.every((i) => !i.unknown &&
|
|
865
|
-
next.actions.filter((a) => a.kind === i.kind && a.status === "prepared").length >=
|
|
866
|
-
i.planned - i.verified) &&
|
|
867
|
-
waits.every((w) => w && Date.parse(w.resumeAt) > clock()) &&
|
|
868
|
-
Date.parse(scheduled) > clock()) {
|
|
869
|
-
for (let i = 0; i < pending.length; i++) {
|
|
870
|
-
const proof = await store.recordProof({
|
|
871
|
-
observedAt: stamp(),
|
|
872
|
-
...waits[i],
|
|
873
|
-
attempts: next.attempts,
|
|
874
|
-
});
|
|
875
|
-
Object.assign(pending[i], {
|
|
876
|
-
actionable: false,
|
|
877
|
-
reason: `harness:${waits[i].reason}`,
|
|
878
|
-
resumeAt: waits[i].resumeAt,
|
|
879
|
-
resumeWhen: `不早于${waits[i].resumeAt}重核`,
|
|
880
|
-
blockEvidence: [proof],
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
own.status = "WAITING";
|
|
884
|
-
own.stop = {
|
|
885
|
-
independentActionsHandled: true,
|
|
886
|
-
nextScheduledCheckAt: scheduled,
|
|
887
|
-
nextEligibleCheckAt: waits.map((w) => w.resumeAt).sort()[0],
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
next.status = next.items.every((i) => i.verified === i.planned && !i.unknown)
|
|
891
|
-
? "COMPLETE"
|
|
892
|
-
: "RUNNING";
|
|
893
|
-
own.updatedAt = next.updatedAt = stamp();
|
|
894
|
-
return next;
|
|
895
|
-
});
|
|
896
|
-
const out = result(snapshot, needs);
|
|
897
|
-
if (!out.mayEnd && !needs.length && completed >= maxActions)
|
|
898
|
-
return {
|
|
899
|
-
...out,
|
|
900
|
-
status: "checkpoint",
|
|
901
|
-
checkpoint: {
|
|
902
|
-
taskId,
|
|
903
|
-
executorId: executor.id,
|
|
904
|
-
completed,
|
|
905
|
-
resume: "resumeGrowthRound",
|
|
906
|
-
},
|
|
907
|
-
};
|
|
908
|
-
if (!out.mayEnd && !needs.length)
|
|
909
|
-
out.decision = [
|
|
910
|
-
{ reason: "prepared-queue-does-not-cover-original-targets" },
|
|
911
|
-
];
|
|
912
|
-
return out;
|
|
913
|
-
}
|
|
914
|
-
catch (error) {
|
|
915
|
-
return {
|
|
916
|
-
status: "needsDecision",
|
|
917
|
-
mayEnd: false,
|
|
918
|
-
taskId,
|
|
919
|
-
evidenceRef: store.path,
|
|
920
|
-
decision: [
|
|
921
|
-
{
|
|
922
|
-
reason: "harness-boundary-error",
|
|
923
|
-
error: error.message.slice(0, 240),
|
|
924
|
-
},
|
|
925
|
-
],
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
finally {
|
|
929
|
-
try {
|
|
930
|
-
await releaseSession(taskId);
|
|
931
|
-
}
|
|
932
|
-
finally {
|
|
933
|
-
running = false;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
async function round({ taskId, decision, selector, requestedKind, } = {}) {
|
|
938
|
-
try {
|
|
939
|
-
return await store.withLock(async () => {
|
|
940
|
-
let snapshot = await load(taskId);
|
|
941
|
-
const activeKinds = snapshot.state.scopeKinds ?? kinds;
|
|
942
|
-
if (requestedKind && !activeKinds.includes(requestedKind))
|
|
943
|
-
return {
|
|
944
|
-
...result(snapshot),
|
|
945
|
-
status: "needsDecision",
|
|
946
|
-
mayEnd: false,
|
|
947
|
-
decision: [
|
|
948
|
-
{ kind: requestedKind, reason: "kind-outside-round-scope" },
|
|
949
|
-
],
|
|
950
|
-
};
|
|
951
|
-
if (decision?.type === "prepareActions") {
|
|
952
|
-
if (!Array.isArray(decision.actions) ||
|
|
953
|
-
!/^[A-Za-z0-9_]{1,15}$/.test(decision.accountHandle ?? "") ||
|
|
954
|
-
!evidenceExists(decision.ownershipEvidenceRef) ||
|
|
955
|
-
(snapshot.state.execution?.owner &&
|
|
956
|
-
snapshot.state.execution.owner !== "harness"))
|
|
957
|
-
throw new Error("需原账号执行权核验和完整动作队列;不接管已有执行者");
|
|
958
|
-
if (snapshot.state.accountHandle &&
|
|
959
|
-
!same(snapshot.state.accountHandle, decision.accountHandle))
|
|
960
|
-
throw new Error("不能变更原账号");
|
|
961
|
-
const next = structuredClone(snapshot.state);
|
|
962
|
-
next.actions ??= [];
|
|
963
|
-
for (const action of decision.actions) {
|
|
964
|
-
if (!action.id ||
|
|
965
|
-
!activeKinds.includes(action.kind) ||
|
|
966
|
-
action.status !== "prepared" ||
|
|
967
|
-
next.actions.some((a) => a.id === action.id))
|
|
968
|
-
throw new Error("仅添加职责范围内唯一ID的prepared原动作");
|
|
969
|
-
next.actions.push(structuredClone(action));
|
|
970
|
-
}
|
|
971
|
-
if (next.items.some((item) => next.actions.filter((a) => a.kind === item.kind && a.status === "prepared").length >
|
|
972
|
-
item.planned - item.verified - item.unknown))
|
|
973
|
-
throw new Error("prepared队列不能越过原批次未决目标");
|
|
974
|
-
next.accountHandle = decision.accountHandle;
|
|
975
|
-
next.execution = {
|
|
976
|
-
...next.execution,
|
|
977
|
-
owner: "harness",
|
|
978
|
-
exclusive: true,
|
|
979
|
-
ownershipEvidenceRef: decision.ownershipEvidenceRef,
|
|
980
|
-
...(decision.nextScheduledCheckAt
|
|
981
|
-
? { nextScheduledCheckAt: decision.nextScheduledCheckAt }
|
|
982
|
-
: {}),
|
|
983
|
-
};
|
|
984
|
-
if (decision.attemptsCoverage) {
|
|
985
|
-
if (!decision.attemptsCoverage.complete ||
|
|
986
|
-
!evidenceExists(decision.attemptsCoverage.evidenceRef))
|
|
987
|
-
throw new Error("共享历史须完整取证;不能默认为零");
|
|
988
|
-
next.attemptsCoverage = structuredClone(decision.attemptsCoverage);
|
|
989
|
-
}
|
|
990
|
-
if (decision.sharedAttempts) {
|
|
991
|
-
if (!Array.isArray(decision.sharedAttempts))
|
|
992
|
-
throw new Error("共享尝试记录必须为数组");
|
|
993
|
-
next.attempts = [
|
|
994
|
-
...(next.attempts ?? []),
|
|
995
|
-
...structuredClone(decision.sharedAttempts),
|
|
996
|
-
];
|
|
997
|
-
}
|
|
998
|
-
snapshot = await save(snapshot, next);
|
|
999
|
-
}
|
|
1000
|
-
else if (decision) {
|
|
1001
|
-
const action = snapshot.state.actions?.find((a) => a.id === decision.actionId);
|
|
1002
|
-
if (action && !activeKinds.includes(action.kind))
|
|
1003
|
-
throw new Error("kind-outside-round-scope");
|
|
1004
|
-
if (!action ||
|
|
1005
|
-
action.status !== "prepared" ||
|
|
1006
|
-
typeof decision.approved !== "boolean" ||
|
|
1007
|
-
!evidenceExists(decision.evidenceRef))
|
|
1008
|
-
throw new Error("决策仅能绑定原prepared动作及真实审查证据");
|
|
1009
|
-
const next = structuredClone(snapshot.state), target = next.actions.find((a) => a.id === action.id);
|
|
1010
|
-
if (!decision.approved)
|
|
1011
|
-
target.status = "skipped";
|
|
1012
|
-
else
|
|
1013
|
-
target.quality = {
|
|
1014
|
-
approved: true,
|
|
1015
|
-
evidenceRef: decision.evidenceRef,
|
|
1016
|
-
...(action.kind === "follow"
|
|
1017
|
-
? {
|
|
1018
|
-
handle: action.handle,
|
|
1019
|
-
blue: decision.blue === true,
|
|
1020
|
-
chinese: decision.chinese === true,
|
|
1021
|
-
}
|
|
1022
|
-
: {
|
|
1023
|
-
approvedText: decision.approvedText,
|
|
1024
|
-
...(action.kind === "original"
|
|
1025
|
-
? { draftFingerprint: decision.draftFingerprint }
|
|
1026
|
-
: {}),
|
|
1027
|
-
}),
|
|
1028
|
-
};
|
|
1029
|
-
snapshot = await save(snapshot, next);
|
|
1030
|
-
}
|
|
1031
|
-
const needs = [];
|
|
1032
|
-
for (const kind of activeKinds) {
|
|
1033
|
-
const queue = (snapshot.state.actions ?? [])
|
|
1034
|
-
.filter((a) => a.kind === kind && (!selector || selector(a)))
|
|
1035
|
-
.sort((a, b) => Number(["intent", "unknown"].includes(b.status)) -
|
|
1036
|
-
Number(["intent", "unknown"].includes(a.status)));
|
|
1037
|
-
// A selector narrows this invocation, not the original unresolved ledger.
|
|
1038
|
-
const unresolved = (snapshot.state.actions ?? []).filter((a) => a.kind === kind && ["intent", "unknown"].includes(a.status)).length;
|
|
1039
|
-
if (snapshot.state.items.find((i) => i.kind === kind).unknown >
|
|
1040
|
-
unresolved) {
|
|
1041
|
-
needs.push({
|
|
1042
|
-
status: "needsDecision",
|
|
1043
|
-
reason: "original-unmapped-unknown-needs-dedup",
|
|
1044
|
-
kind,
|
|
1045
|
-
});
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
for (const original of queue) {
|
|
1049
|
-
const item = snapshot.state.items.find((i) => i.kind === kind);
|
|
1050
|
-
const action = snapshot.state.actions.find((a) => a.id === original.id);
|
|
1051
|
-
if (["verified", "preexisting", "skipped"].includes(action.status))
|
|
1052
|
-
continue;
|
|
1053
|
-
if (item.verified >= item.planned && action.status === "prepared")
|
|
1054
|
-
continue;
|
|
1055
|
-
const outcome = await execute(snapshot, action);
|
|
1056
|
-
snapshot = outcome.snapshot;
|
|
1057
|
-
if (outcome.need) {
|
|
1058
|
-
needs.push(outcome.need);
|
|
1059
|
-
break;
|
|
1060
|
-
} // Stop only this kind; continue independent kinds.
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
if (snapshot.state.items
|
|
1064
|
-
.filter((i) => activeKinds.includes(i.kind))
|
|
1065
|
-
.every((i) => i.verified === i.planned && i.unknown === 0)) {
|
|
1066
|
-
const next = structuredClone(snapshot.state);
|
|
1067
|
-
next.status = "COMPLETE";
|
|
1068
|
-
snapshot = await save(snapshot, next);
|
|
1069
|
-
}
|
|
1070
|
-
else {
|
|
1071
|
-
const pending = snapshot.state.items.filter((i) => activeKinds.includes(i.kind) &&
|
|
1072
|
-
(i.verified < i.planned || i.unknown > 0));
|
|
1073
|
-
const waits = pending.map((item) => needs.find((n) => n.status === "waiting" &&
|
|
1074
|
-
snapshot.state.actions.find((a) => a.id === n.actionId)
|
|
1075
|
-
?.kind === item.kind));
|
|
1076
|
-
const scheduled = snapshot.state.execution?.nextScheduledCheckAt ??
|
|
1077
|
-
snapshot.state.stop?.nextScheduledCheckAt;
|
|
1078
|
-
const covered = pending.every((item) => item.unknown === 0 &&
|
|
1079
|
-
(snapshot.state.actions ?? []).filter((a) => a.kind === item.kind && a.status === "prepared").length >=
|
|
1080
|
-
item.planned - item.verified);
|
|
1081
|
-
if (covered &&
|
|
1082
|
-
waits.every((w) => w && Date.parse(w.resumeAt) > clock()) &&
|
|
1083
|
-
Date.parse(scheduled) > clock()) {
|
|
1084
|
-
const next = structuredClone(snapshot.state);
|
|
1085
|
-
for (let i = 0; i < pending.length; i++) {
|
|
1086
|
-
const item = next.items.find((a) => a.kind === pending[i].kind), wait = waits[i];
|
|
1087
|
-
const proof = await store.recordProof({
|
|
1088
|
-
observedAt: stamp(),
|
|
1089
|
-
reason: wait.reason,
|
|
1090
|
-
resumeAt: wait.resumeAt,
|
|
1091
|
-
attempts: next.attempts,
|
|
1092
|
-
});
|
|
1093
|
-
Object.assign(item, {
|
|
1094
|
-
actionable: false,
|
|
1095
|
-
reason: `harness:${wait.reason}`,
|
|
1096
|
-
resumeAt: wait.resumeAt,
|
|
1097
|
-
resumeWhen: `不早于${wait.resumeAt}重核预算及fresh状态`,
|
|
1098
|
-
blockEvidence: [proof],
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
next.status = "WAITING";
|
|
1102
|
-
next.stop = {
|
|
1103
|
-
independentActionsHandled: true,
|
|
1104
|
-
nextScheduledCheckAt: scheduled,
|
|
1105
|
-
nextEligibleCheckAt: waits.map((w) => w.resumeAt).sort()[0],
|
|
1106
|
-
};
|
|
1107
|
-
snapshot = await save(snapshot, next);
|
|
1108
|
-
}
|
|
1109
|
-
else if (!needs.length)
|
|
1110
|
-
needs.push({
|
|
1111
|
-
status: "needsDecision",
|
|
1112
|
-
reason: "prepared-queue-does-not-cover-original-targets",
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
return result(snapshot, needs);
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
catch (error) {
|
|
1119
|
-
return {
|
|
1120
|
-
status: "needsDecision",
|
|
1121
|
-
mayEnd: false,
|
|
1122
|
-
taskId,
|
|
1123
|
-
evidenceRef: store.path,
|
|
1124
|
-
decision: [
|
|
1125
|
-
{
|
|
1126
|
-
reason: error.code === "CONFLICT"
|
|
1127
|
-
? "original-ledger-conflict"
|
|
1128
|
-
: "harness-boundary-error",
|
|
1129
|
-
error: error.message.slice(0, 240),
|
|
1130
|
-
},
|
|
1131
|
-
],
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
// Independent tabs share only short ledger transactions, never a browser-operation lock.
|
|
1136
|
-
async function publishIndependent({ taskId, draftPath, preparedAction, independentExecution, }) {
|
|
1137
|
-
let action, token, submissionStarted = false;
|
|
1138
|
-
const inWindow = () => {
|
|
1139
|
-
const hour = Number(new Intl.DateTimeFormat("en-GB", {
|
|
1140
|
-
timeZone: "Asia/Shanghai",
|
|
1141
|
-
hour: "2-digit",
|
|
1142
|
-
hourCycle: "h23",
|
|
1143
|
-
}).format(clock()));
|
|
1144
|
-
return hour >= 7 && hour < 22;
|
|
1145
|
-
};
|
|
1146
|
-
try {
|
|
1147
|
-
const caps = capability("submitPost", preparedAction ?? {});
|
|
1148
|
-
if (!caps.covered)
|
|
1149
|
-
return { status: "needsCapability", ...caps };
|
|
1150
|
-
if (!inWindow())
|
|
1151
|
-
throw new Error("outside-contract-write-window");
|
|
1152
|
-
await store.withLock(async () => {
|
|
1153
|
-
const snapshot = await load(taskId), state = structuredClone(snapshot.state);
|
|
1154
|
-
state.actions ??= [];
|
|
1155
|
-
action = state.actions.find((a) => a.kind === "original" && a.draftPath === draftPath);
|
|
1156
|
-
if (!action) {
|
|
1157
|
-
if (!preparedAction?.id ||
|
|
1158
|
-
preparedAction.kind !== "original" ||
|
|
1159
|
-
preparedAction.status !== "prepared" ||
|
|
1160
|
-
preparedAction.draftPath !== draftPath ||
|
|
1161
|
-
state.actions.some((a) => a.id === preparedAction.id))
|
|
1162
|
-
throw new Error("unique-prepared-original-required");
|
|
1163
|
-
const error = guardInput(state, preparedAction, independentExecution);
|
|
1164
|
-
if (error)
|
|
1165
|
-
throw new Error(error);
|
|
1166
|
-
action = structuredClone(preparedAction);
|
|
1167
|
-
state.actions.push(action);
|
|
1168
|
-
await save(snapshot, state);
|
|
1169
|
-
}
|
|
1170
|
-
if (action.status !== "prepared")
|
|
1171
|
-
throw new Error("existing-submission-do-not-resubmit");
|
|
1172
|
-
if (action.claim && store.ownerAlive(action.claim.pid))
|
|
1173
|
-
throw new Error("same-action-claimed");
|
|
1174
|
-
const error = guardInput(state, action, independentExecution);
|
|
1175
|
-
if (error)
|
|
1176
|
-
throw new Error(error);
|
|
1177
|
-
});
|
|
1178
|
-
const own = await platform.readAccount();
|
|
1179
|
-
if (own.needsStateCheck ||
|
|
1180
|
-
own.alerts?.length ||
|
|
1181
|
-
!same(own.accountHandle, action.accountHandle))
|
|
1182
|
-
throw new Error("fresh-account-unconfirmed");
|
|
1183
|
-
const baseline = await platform.findSubmission({
|
|
1184
|
-
...action,
|
|
1185
|
-
intentAt: stamp(),
|
|
1186
|
-
});
|
|
1187
|
-
if (baseline.needsStateCheck || baseline.alerts?.length)
|
|
1188
|
-
throw new Error("publication-baseline-unconfirmed");
|
|
1189
|
-
token = `${action.id}:${stamp()}:${Math.random()}`;
|
|
1190
|
-
await store.withLock(async () => {
|
|
1191
|
-
const snapshot = await load(taskId), state = structuredClone(snapshot.state);
|
|
1192
|
-
const target = state.actions.find((a) => a.id === action.id), item = state.items.find((i) => i.kind === "original");
|
|
1193
|
-
if (target?.status !== "prepared" ||
|
|
1194
|
-
(target.claim && store.ownerAlive(target.claim.pid)))
|
|
1195
|
-
throw new Error("action-already-claimed-do-not-resubmit");
|
|
1196
|
-
const error = guardInput(state, target, independentExecution);
|
|
1197
|
-
if (error)
|
|
1198
|
-
throw new Error(error);
|
|
1199
|
-
if (!inWindow() || item.verified + item.unknown >= item.planned)
|
|
1200
|
-
throw new Error("window-or-original-capacity-unavailable");
|
|
1201
|
-
Object.assign(target, {
|
|
1202
|
-
status: "intent",
|
|
1203
|
-
intentAt: stamp(),
|
|
1204
|
-
beforeIds: (baseline.posts ?? [])
|
|
1205
|
-
.map((p) => p.id)
|
|
1206
|
-
.filter(Boolean),
|
|
1207
|
-
claimToken: token,
|
|
1208
|
-
independentExecution,
|
|
1209
|
-
claim: {
|
|
1210
|
-
actionId: target.id,
|
|
1211
|
-
token,
|
|
1212
|
-
pid: store.processId,
|
|
1213
|
-
tabId: independentExecution.tabId,
|
|
1214
|
-
acquiredAt: stamp(),
|
|
1215
|
-
},
|
|
1216
|
-
});
|
|
1217
|
-
item.unknown += 1;
|
|
1218
|
-
state.attempts ??= [];
|
|
1219
|
-
state.attempts.push({
|
|
1220
|
-
actionId: target.id,
|
|
1221
|
-
kind: "original",
|
|
1222
|
-
at: target.intentAt,
|
|
1223
|
-
});
|
|
1224
|
-
await save(snapshot, state);
|
|
1225
|
-
action = structuredClone(target);
|
|
1226
|
-
});
|
|
1227
|
-
let observation;
|
|
1228
|
-
try {
|
|
1229
|
-
await platform.submitPost({
|
|
1230
|
-
...action,
|
|
1231
|
-
beforeSubmit: async () => {
|
|
1232
|
-
const latest = await load(taskId), target = latest.state.actions.find((a) => a.id === action.id);
|
|
1233
|
-
if (target?.status !== "intent" ||
|
|
1234
|
-
target.claimToken !== token ||
|
|
1235
|
-
!inWindow())
|
|
1236
|
-
throw new Error("action-claim-or-window-changed");
|
|
1237
|
-
const error = guardInput(latest.state, target, independentExecution);
|
|
1238
|
-
if (error)
|
|
1239
|
-
throw new Error(error);
|
|
1240
|
-
submissionStarted = true;
|
|
1241
|
-
},
|
|
1242
|
-
});
|
|
1243
|
-
observation = await platform.findSubmission(action);
|
|
1244
|
-
}
|
|
1245
|
-
catch (error) {
|
|
1246
|
-
if (!submissionStarted)
|
|
1247
|
-
throw error;
|
|
1248
|
-
observation = {
|
|
1249
|
-
verified: false,
|
|
1250
|
-
missing: ["submission-or-verification-failed"],
|
|
1251
|
-
error: error.message.slice(0, 240),
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
return await store.withLock(async () => {
|
|
1255
|
-
const snapshot = await load(taskId), target = snapshot.state.actions.find((a) => a.id === action.id);
|
|
1256
|
-
if (target?.claimToken !== token || target.status !== "intent")
|
|
1257
|
-
throw new Error("action-settlement-conflict");
|
|
1258
|
-
await settleProof(snapshot, target, observation, {
|
|
1259
|
-
preserveStatus: true,
|
|
1260
|
-
});
|
|
1261
|
-
return {
|
|
1262
|
-
status: observation.verified ? "verified" : "unknown",
|
|
1263
|
-
taskId,
|
|
1264
|
-
actionId: action.id,
|
|
1265
|
-
publicId: observation.publicId,
|
|
1266
|
-
evidenceRef: store.path,
|
|
1267
|
-
mayEnd: false,
|
|
1268
|
-
};
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
1271
|
-
catch (error) {
|
|
1272
|
-
if (token && !submissionStarted)
|
|
1273
|
-
await store.withLock(async () => {
|
|
1274
|
-
const snapshot = await load(taskId), state = structuredClone(snapshot.state), target = state.actions.find((a) => a.id === action.id);
|
|
1275
|
-
if (target?.claimToken !== token || target.status !== "intent")
|
|
1276
|
-
return;
|
|
1277
|
-
target.status = "prepared";
|
|
1278
|
-
target.preparationEvidence = await store.recordProof({
|
|
1279
|
-
observedAt: stamp(),
|
|
1280
|
-
submissionStarted: false,
|
|
1281
|
-
error: error.message.slice(0, 240),
|
|
1282
|
-
});
|
|
1283
|
-
state.items.find((i) => i.kind === "original").unknown -= 1;
|
|
1284
|
-
await save(snapshot, state);
|
|
1285
|
-
});
|
|
1286
|
-
return {
|
|
1287
|
-
status: "needsDecision",
|
|
1288
|
-
taskId,
|
|
1289
|
-
mayEnd: false,
|
|
1290
|
-
decision: [
|
|
1291
|
-
{
|
|
1292
|
-
reason: error.code === "CONFLICT"
|
|
1293
|
-
? "original-ledger-conflict"
|
|
1294
|
-
: error.message.slice(0, 240),
|
|
1295
|
-
},
|
|
1296
|
-
],
|
|
1297
|
-
};
|
|
1298
|
-
}
|
|
1299
|
-
finally {
|
|
1300
|
-
if (token)
|
|
1301
|
-
await store.transaction((snapshot) => {
|
|
1302
|
-
if (snapshot.state.batchId !== taskId)
|
|
1303
|
-
return null;
|
|
1304
|
-
const next = structuredClone(snapshot.state), target = next.actions.find((a) => a.id === action.id);
|
|
1305
|
-
if (target?.claim?.token !== token)
|
|
1306
|
-
return null;
|
|
1307
|
-
delete target.claim;
|
|
1308
|
-
return next;
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
async function releaseSession(taskId = undefined) {
|
|
1313
|
-
if (!executor)
|
|
1314
|
-
return;
|
|
1315
|
-
await store.transaction((latest) => {
|
|
1316
|
-
const own = latest.state.executions?.[executor.id];
|
|
1317
|
-
if ((taskId && latest.state.batchId !== taskId) ||
|
|
1318
|
-
own?.runtimeId !== runtimeId ||
|
|
1319
|
-
!own.activity)
|
|
1320
|
-
return null;
|
|
1321
|
-
const next = structuredClone(latest.state);
|
|
1322
|
-
delete next.executions[executor.id].activity;
|
|
1323
|
-
next.updatedAt = stamp();
|
|
1324
|
-
return next;
|
|
123
|
+
this.#execute = execute;
|
|
124
|
+
const { parallelRound } = createParallelRound({
|
|
125
|
+
load: this.#load,
|
|
126
|
+
scope: this.#scope,
|
|
127
|
+
result: this.#result,
|
|
128
|
+
store: this.#store,
|
|
129
|
+
executor: this.#executor,
|
|
130
|
+
runtimeId: this.#runtimeId,
|
|
131
|
+
stamp: this.stamp.bind(this),
|
|
132
|
+
evidenceExists: this.#evidenceExists,
|
|
133
|
+
execute: this.#execute,
|
|
134
|
+
clock: this.#clock,
|
|
135
|
+
releaseSession: this.#releaseSession,
|
|
136
|
+
session: this.#session,
|
|
1325
137
|
});
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
138
|
+
this.#parallelRound = parallelRound;
|
|
139
|
+
const { round } = createPlannedRound({
|
|
140
|
+
store: this.#store,
|
|
141
|
+
load: this.#load,
|
|
142
|
+
result: this.#result,
|
|
143
|
+
evidenceExists: this.#evidenceExists,
|
|
144
|
+
save: this.#save,
|
|
145
|
+
execute: this.#execute,
|
|
146
|
+
clock: this.#clock,
|
|
147
|
+
stamp: this.stamp.bind(this),
|
|
148
|
+
});
|
|
149
|
+
this.#round = round;
|
|
150
|
+
const { publishIndependent, prepareActionsForCall } = createIndependentPublishing({
|
|
151
|
+
clock: this.#clock,
|
|
152
|
+
evidenceExists: this.#evidenceExists,
|
|
153
|
+
capability: this.#capability,
|
|
154
|
+
store: this.#store,
|
|
155
|
+
load: this.#load,
|
|
156
|
+
guardInput: this.#guardInput,
|
|
157
|
+
save: this.#save,
|
|
158
|
+
platform: this.#platform,
|
|
159
|
+
stamp: this.stamp.bind(this),
|
|
160
|
+
settleProof: this.#settleProof,
|
|
161
|
+
});
|
|
162
|
+
this.#publishIndependent = publishIndependent;
|
|
163
|
+
this.#prepareActionsForCall = prepareActionsForCall;
|
|
164
|
+
const { readTask } = createGrowthReading({
|
|
165
|
+
platform: this.#platform,
|
|
166
|
+
executor: this.#executor,
|
|
167
|
+
store: this.#store,
|
|
168
|
+
runtimeId: this.#runtimeId,
|
|
169
|
+
stamp: this.stamp.bind(this),
|
|
170
|
+
releaseSession: this.#releaseSession,
|
|
171
|
+
session: this.#session,
|
|
1341
172
|
});
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
status: "needsCapability",
|
|
1345
|
-
...caps,
|
|
1346
|
-
missing: [
|
|
1347
|
-
...(caps.missing ?? []),
|
|
1348
|
-
...(typeof platform[operation] === "function"
|
|
1349
|
-
? []
|
|
1350
|
-
: [`platform.${operation}`]),
|
|
1351
|
-
],
|
|
1352
|
-
};
|
|
1353
|
-
running = true;
|
|
1354
|
-
try {
|
|
1355
|
-
if (executor)
|
|
1356
|
-
await store.transaction((latest) => {
|
|
1357
|
-
const next = structuredClone(latest.state);
|
|
1358
|
-
next.executions ??= {};
|
|
1359
|
-
if (Object.values(next.executions).some((e) => (e.tabId === executor.tabId || e.id === executor.id) &&
|
|
1360
|
-
e.runtimeId !== runtimeId &&
|
|
1361
|
-
(e.activity || e.actionId) &&
|
|
1362
|
-
store.ownerAlive(e.pid)))
|
|
1363
|
-
throw new Error("same-tab-or-executor-claimed");
|
|
1364
|
-
next.executions[executor.id] = {
|
|
1365
|
-
...next.executions[executor.id],
|
|
1366
|
-
...executor,
|
|
1367
|
-
runtimeId,
|
|
1368
|
-
pid: store.processId,
|
|
1369
|
-
activity: operation,
|
|
1370
|
-
status: next.executions[executor.id]?.status ?? "RUNNING",
|
|
1371
|
-
updatedAt: stamp(),
|
|
1372
|
-
};
|
|
1373
|
-
next.updatedAt = stamp();
|
|
1374
|
-
return next;
|
|
1375
|
-
});
|
|
1376
|
-
const value = await platform[operation](input);
|
|
1377
|
-
const evidenceRef = await store.recordProof(await platform.captureProof({ observation: value }));
|
|
1378
|
-
const status = value.status ??
|
|
1379
|
-
(value.complete === false ||
|
|
1380
|
-
value.needsStateCheck ||
|
|
1381
|
-
value.alerts?.length
|
|
1382
|
-
? "needsDecision"
|
|
1383
|
-
: "observed");
|
|
1384
|
-
const receipt = {
|
|
1385
|
-
status,
|
|
1386
|
-
fresh: true,
|
|
1387
|
-
evidenceRef,
|
|
1388
|
-
complete: status === "observed" && (value.complete ?? !value.needsStateCheck),
|
|
1389
|
-
...((value.sources ?? value.feedback)
|
|
1390
|
-
? { ids: (value.sources ?? value.feedback).map((p) => p.id) }
|
|
1391
|
-
: {}),
|
|
1392
|
-
...(operation === "readAccount"
|
|
1393
|
-
? {
|
|
1394
|
-
accountHandle: value.accountHandle ?? null,
|
|
1395
|
-
accountURL: value.accountURL ?? null,
|
|
1396
|
-
}
|
|
1397
|
-
: {}),
|
|
1398
|
-
...(value.missing?.length ? { missing: value.missing } : {}),
|
|
1399
|
-
};
|
|
1400
|
-
if (mode === "receipt")
|
|
1401
|
-
return receipt;
|
|
1402
|
-
const collection = value.sources
|
|
1403
|
-
? "sources"
|
|
1404
|
-
: value.feedback
|
|
1405
|
-
? "feedback"
|
|
1406
|
-
: null;
|
|
1407
|
-
if (mode === "delta" && collection) {
|
|
1408
|
-
const unchangedIds = [], changed = [];
|
|
1409
|
-
for (const entry of value[collection]) {
|
|
1410
|
-
const key = JSON.stringify([operation, input, entry.id]);
|
|
1411
|
-
const normalized = JSON.stringify(entry, (name, val) => name === "observedAt" ? undefined : val);
|
|
1412
|
-
if (readCache.get(key) === normalized)
|
|
1413
|
-
unchangedIds.push(entry.id);
|
|
1414
|
-
else
|
|
1415
|
-
changed.push(entry);
|
|
1416
|
-
readCache.set(key, normalized);
|
|
1417
|
-
}
|
|
1418
|
-
return { ...receipt, [collection]: changed, unchangedIds };
|
|
1419
|
-
}
|
|
1420
|
-
return mode === "delta" ? receipt : { ...value, ...receipt };
|
|
1421
|
-
}
|
|
1422
|
-
catch (error) {
|
|
1423
|
-
const category = /connection|transport|interrupted|extension/i.test(error.message)
|
|
1424
|
-
? "browser-connection"
|
|
1425
|
-
: "ui-not-confirmed";
|
|
1426
|
-
const code = error.code === "CONFLICT"
|
|
1427
|
-
? "ledger-conflict"
|
|
1428
|
-
: /same-tab-or-executor-claimed/.test(error.message)
|
|
1429
|
-
? "tab-busy"
|
|
1430
|
-
: category === "browser-connection"
|
|
1431
|
-
? "connection-failed"
|
|
1432
|
-
: "observation-unconfirmed";
|
|
1433
|
-
const diagnostic = { operation, category, code };
|
|
1434
|
-
const evidenceRef = await store.recordProof({
|
|
1435
|
-
capturedAt: stamp(),
|
|
1436
|
-
diagnostic: { ...diagnostic, error: error.message },
|
|
1437
|
-
});
|
|
1438
|
-
return {
|
|
1439
|
-
status: "needsDecision",
|
|
1440
|
-
fresh: false,
|
|
1441
|
-
complete: false,
|
|
1442
|
-
evidenceRef,
|
|
1443
|
-
diagnostic: mode === "decision"
|
|
1444
|
-
? { ...diagnostic, error: error.message.slice(0, 240) }
|
|
1445
|
-
: diagnostic,
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
finally {
|
|
1449
|
-
try {
|
|
1450
|
-
await releaseSession();
|
|
1451
|
-
}
|
|
1452
|
-
finally {
|
|
1453
|
-
running = false;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
173
|
+
this.#readTask = readTask;
|
|
174
|
+
this.composeTasks();
|
|
1456
175
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
action.status !== "prepared" ||
|
|
1471
|
-
!kinds.includes(action.kind))
|
|
1472
|
-
throw new Error("unique-prepared-action-required");
|
|
1473
|
-
ids.add(action.id);
|
|
1474
|
-
const inputError = guardInput(next, action);
|
|
1475
|
-
if (inputError)
|
|
1476
|
-
throw new Error(inputError);
|
|
1477
|
-
const existing = next.actions.find((a) => a.id === action.id);
|
|
1478
|
-
if (existing) {
|
|
1479
|
-
const fields = [
|
|
1480
|
-
"kind",
|
|
1481
|
-
"accountHandle",
|
|
1482
|
-
"handle",
|
|
1483
|
-
"postId",
|
|
1484
|
-
"text",
|
|
1485
|
-
"draftPath",
|
|
1486
|
-
];
|
|
1487
|
-
if (fields.some((field) => existing[field] !== action[field]))
|
|
1488
|
-
throw new Error("original-action-input-changed");
|
|
1489
|
-
continue; // intent/unknown/verified stay untouched and follow their original recovery.
|
|
1490
|
-
}
|
|
1491
|
-
const duplicate = next.actions.some((a) => a.kind === action.kind &&
|
|
1492
|
-
(action.kind === "follow"
|
|
1493
|
-
? same(a.handle, action.handle)
|
|
1494
|
-
: action.kind === "original"
|
|
1495
|
-
? a.draftPath === action.draftPath
|
|
1496
|
-
: a.postId === action.postId && a.text === action.text));
|
|
1497
|
-
if (duplicate)
|
|
1498
|
-
throw new Error("business-object-already-prepared");
|
|
1499
|
-
const item = next.items.find((i) => i.kind === action.kind);
|
|
1500
|
-
const queued = next.actions.filter((a) => a.kind === action.kind &&
|
|
1501
|
-
!["skipped", "preexisting"].includes(a.status)).length;
|
|
1502
|
-
if (!item || queued >= item.planned)
|
|
1503
|
-
throw new Error("prepared-actions-exceed-original-target");
|
|
1504
|
-
next.actions.push(structuredClone(action));
|
|
1505
|
-
}
|
|
1506
|
-
next.updatedAt = stamp();
|
|
1507
|
-
return next;
|
|
176
|
+
composeTasks() {
|
|
177
|
+
this.#createGrowthRecoveryResult = createGrowthRecovery({
|
|
178
|
+
capability: this.#capability,
|
|
179
|
+
platform: this.#platform,
|
|
180
|
+
load: this.#load,
|
|
181
|
+
evidenceExists: this.#evidenceExists,
|
|
182
|
+
clock: this.#clock,
|
|
183
|
+
store: this.#store,
|
|
184
|
+
settleProof: this.#settleProof,
|
|
185
|
+
executor: this.#executor,
|
|
186
|
+
parallelRound: this.#parallelRound,
|
|
187
|
+
round: this.#round,
|
|
188
|
+
readTask: this.#readTask,
|
|
1508
189
|
});
|
|
190
|
+
this.#createFollowBatchResult = createFollowBatch({
|
|
191
|
+
prepareActionsForCall: this.#prepareActionsForCall,
|
|
192
|
+
store: this.#store,
|
|
193
|
+
scope: this.#scope,
|
|
194
|
+
stamp: this.stamp.bind(this),
|
|
195
|
+
clock: this.#clock,
|
|
196
|
+
evidenceExists: this.#evidenceExists,
|
|
197
|
+
executor: this.#executor,
|
|
198
|
+
parallelRound: this.#parallelRound,
|
|
199
|
+
round: this.#round,
|
|
200
|
+
});
|
|
201
|
+
this.#createGrowthTasksResult = createGrowthTasks({
|
|
202
|
+
platform: this.#platform,
|
|
203
|
+
capability: this.#capability,
|
|
204
|
+
executor: this.#executor,
|
|
205
|
+
readTask: this.#readTask,
|
|
206
|
+
parallelRound: this.#parallelRound,
|
|
207
|
+
round: this.#round,
|
|
208
|
+
load: this.#load,
|
|
209
|
+
result: this.#result,
|
|
210
|
+
publishIndependent: this.#publishIndependent,
|
|
211
|
+
prepareActionsForCall: this.#prepareActionsForCall,
|
|
212
|
+
});
|
|
213
|
+
Object.assign(this, Object.assign({}, this.#createGrowthRecoveryResult, this.#createFollowBatchResult, this.#createGrowthTasksResult));
|
|
1509
214
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
status: "needsCapability",
|
|
1517
|
-
missing: ["platform.readPost", ...caps.missing],
|
|
1518
|
-
};
|
|
1519
|
-
const initial = await load(taskId);
|
|
1520
|
-
const action = initial.state.actions.find((a) => a.kind === "original" && a.draftPath === draftPath);
|
|
1521
|
-
if (!action ||
|
|
1522
|
-
!["intent", "unknown"].includes(action.status) ||
|
|
1523
|
-
!evidenceExists(evidenceRef))
|
|
1524
|
-
throw new Error("submitted-original-and-public-proof-required");
|
|
1525
|
-
const url = xURL(publicURL), postId = url.pathname.match(/\/status\/(\d+)$/)?.[1];
|
|
1526
|
-
if (!postId ||
|
|
1527
|
-
url.pathname.toLowerCase() !==
|
|
1528
|
-
`/${action.accountHandle}/status/${postId}`.toLowerCase())
|
|
1529
|
-
throw new Error("public-account-mismatch");
|
|
1530
|
-
const observation = await platform.readPost({
|
|
1531
|
-
postId,
|
|
1532
|
-
url: publicURL,
|
|
1533
|
-
evidenceRef,
|
|
1534
|
-
});
|
|
1535
|
-
const trailing = action.text.match(/\n\n(https:\/\/\S+)$/);
|
|
1536
|
-
const matches = (observation.posts ?? []).filter((p) => {
|
|
1537
|
-
const exact = p.body === action.text;
|
|
1538
|
-
const card = trailing &&
|
|
1539
|
-
sourceLink &&
|
|
1540
|
-
evidenceExists(sourceLink.evidenceRef) &&
|
|
1541
|
-
sourceLink.resolvedURL === trailing[1] &&
|
|
1542
|
-
p.links?.includes(sourceLink.href) &&
|
|
1543
|
-
p.body === action.text.slice(0, -trailing[0].length);
|
|
1544
|
-
let renderedSource = false;
|
|
1545
|
-
if (trailing &&
|
|
1546
|
-
sourceLink?.renderedText &&
|
|
1547
|
-
evidenceExists(sourceLink.evidenceRef) &&
|
|
1548
|
-
sourceLink.resolvedURL === trailing[1] &&
|
|
1549
|
-
p.links?.includes(sourceLink.href)) {
|
|
1550
|
-
try {
|
|
1551
|
-
renderedSource =
|
|
1552
|
-
new URL(sourceLink.renderedText.replace(/\s/g, "")).href ===
|
|
1553
|
-
sourceLink.resolvedURL &&
|
|
1554
|
-
p.body?.endsWith(sourceLink.renderedText) &&
|
|
1555
|
-
p.body.slice(0, -sourceLink.renderedText.length).trimEnd() ===
|
|
1556
|
-
action.text.slice(0, -trailing[0].length);
|
|
1557
|
-
}
|
|
1558
|
-
catch {
|
|
1559
|
-
/* Invalid rendered URL cannot verify publication. */
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
return (p.id === postId &&
|
|
1563
|
-
same(p.author, action.accountHandle) &&
|
|
1564
|
-
(exact || card || renderedSource) &&
|
|
1565
|
-
!p.needsExpansion &&
|
|
1566
|
-
!(action.beforeIds ?? []).includes(p.id) &&
|
|
1567
|
-
Date.parse(p.datetime) >= Date.parse(action.intentAt) &&
|
|
1568
|
-
Date.parse(p.datetime) <= clock() + 5000 &&
|
|
1569
|
-
(!action.assets?.length ||
|
|
1570
|
-
action.assets.every((a) => p.images?.some((i) => i.alt === a.alt))));
|
|
1571
|
-
});
|
|
1572
|
-
Object.assign(observation, {
|
|
1573
|
-
verified: !observation.needsStateCheck &&
|
|
1574
|
-
!observation.alerts?.length &&
|
|
1575
|
-
matches.length === 1,
|
|
1576
|
-
publicId: matches.length === 1 ? postId : null,
|
|
1577
|
-
sourceLink,
|
|
1578
|
-
});
|
|
1579
|
-
return store.withLock(async () => {
|
|
1580
|
-
const latest = await load(taskId), target = latest.state.actions.find((a) => a.id === action.id);
|
|
1581
|
-
if (!["intent", "unknown"].includes(target?.status) ||
|
|
1582
|
-
target.intentAt !== action.intentAt ||
|
|
1583
|
-
target.text !== action.text)
|
|
1584
|
-
throw new Error("recovery-action-changed");
|
|
1585
|
-
await settleProof(latest, target, observation, {
|
|
1586
|
-
preserveStatus: true,
|
|
1587
|
-
});
|
|
1588
|
-
return {
|
|
1589
|
-
status: observation.verified ? "verified" : "unknown",
|
|
1590
|
-
publicId: observation.publicId,
|
|
1591
|
-
taskId,
|
|
1592
|
-
actionId: action.id,
|
|
1593
|
-
mayEnd: false,
|
|
1594
|
-
};
|
|
1595
|
-
});
|
|
1596
|
-
},
|
|
1597
|
-
preflight: ({ operation, input = {} }) => {
|
|
1598
|
-
if (["readBookmarks", "searchSources", "readFeedback"].includes(operation))
|
|
1599
|
-
return platform.preflight({ operation, input });
|
|
1600
|
-
if (["inspectRound", "verifySubmission"].includes(operation))
|
|
1601
|
-
return capability("read", input);
|
|
1602
|
-
if (["inspectAccount", "verifyFollow"].includes(operation)) {
|
|
1603
|
-
const name = operation === "verifyFollow" ? "readPerson" : "readAccount";
|
|
1604
|
-
if (typeof platform?.preflight !== "function" ||
|
|
1605
|
-
typeof platform?.[name] !== "function")
|
|
1606
|
-
return {
|
|
1607
|
-
covered: false,
|
|
1608
|
-
missing: [name],
|
|
1609
|
-
owner: "scripts/x-harness-platform.mjs",
|
|
1610
|
-
};
|
|
1611
|
-
return platform.preflight({ operation: "read", input });
|
|
1612
|
-
}
|
|
1613
|
-
if (["runGrowthRound", "resumeGrowthRound"].includes(operation)) {
|
|
1614
|
-
const scoped = executor?.scopeKinds ?? input.scopeKinds ?? kinds;
|
|
1615
|
-
if (!Array.isArray(scoped) ||
|
|
1616
|
-
!scoped.length ||
|
|
1617
|
-
scoped.some((k) => !kinds.includes(k)))
|
|
1618
|
-
return {
|
|
1619
|
-
covered: false,
|
|
1620
|
-
missing: ["scopeKinds"],
|
|
1621
|
-
owner: "scripts/x-growth-harness.mjs",
|
|
1622
|
-
};
|
|
1623
|
-
const all = scoped.map((kind) => capability(operations[kind], { ...input, kind }));
|
|
1624
|
-
return {
|
|
1625
|
-
covered: all.every((c) => c.covered),
|
|
1626
|
-
missing: [...new Set(all.flatMap((c) => c.missing))],
|
|
1627
|
-
owners: [
|
|
1628
|
-
...new Set(all.filter((c) => !c.covered).map((c) => c.owner)),
|
|
1629
|
-
],
|
|
1630
|
-
};
|
|
1631
|
-
}
|
|
1632
|
-
if (![
|
|
1633
|
-
"read",
|
|
1634
|
-
"followOne",
|
|
1635
|
-
"followBatch",
|
|
1636
|
-
"publishPost",
|
|
1637
|
-
"replyToPost",
|
|
1638
|
-
...Object.values(operations),
|
|
1639
|
-
].includes(operation))
|
|
1640
|
-
return {
|
|
1641
|
-
covered: false,
|
|
1642
|
-
missing: [`operation.${operation}`],
|
|
1643
|
-
owner: "scripts/x-growth-harness.mjs",
|
|
1644
|
-
};
|
|
1645
|
-
return capability(operation, input);
|
|
1646
|
-
},
|
|
1647
|
-
readCreator: (args) => readTask("readCreator", args),
|
|
1648
|
-
inspectAccount: (args) => readTask("readAccount", args),
|
|
1649
|
-
readPost: (args) => readTask("readPost", { ...args, expand: true }),
|
|
1650
|
-
readPerson: (args) => readTask("readPerson", args),
|
|
1651
|
-
readBookmarks: (args) => readTask("readBookmarks", args),
|
|
1652
|
-
searchSources: (args) => readTask("searchSources", args),
|
|
1653
|
-
readFeedback: (args) => readTask("readFeedback", args),
|
|
1654
|
-
verifySubmission: async ({ taskId, actionId }) => {
|
|
1655
|
-
const snapshot = await load(taskId), action = snapshot.state.actions?.find((a) => a.id === actionId);
|
|
1656
|
-
if (!action || !["intent", "unknown"].includes(action.status))
|
|
1657
|
-
return {
|
|
1658
|
-
status: "needsDecision",
|
|
1659
|
-
reason: "verification-requires-original-unknown-or-intent",
|
|
1660
|
-
mayEnd: false,
|
|
1661
|
-
};
|
|
1662
|
-
return (executor ? parallelRound : round)({
|
|
1663
|
-
taskId,
|
|
1664
|
-
requestedKind: action.kind,
|
|
1665
|
-
selector: (a) => a.id === actionId,
|
|
1666
|
-
});
|
|
1667
|
-
},
|
|
1668
|
-
verifyFollow: async (target) => {
|
|
1669
|
-
const mode = target.mode ?? "receipt";
|
|
1670
|
-
if (!["receipt", "decision"].includes(mode))
|
|
1671
|
-
throw new Error("关注核验模式须为receipt/decision");
|
|
1672
|
-
const caps = platform.preflight({ operation: "read" });
|
|
1673
|
-
if (!caps.covered)
|
|
1674
|
-
return {
|
|
1675
|
-
handle: target.handle,
|
|
1676
|
-
verified: false,
|
|
1677
|
-
status: "needsCapability",
|
|
1678
|
-
...caps,
|
|
1679
|
-
};
|
|
1680
|
-
if (!target.url || !target.evidenceRef) {
|
|
1681
|
-
const original = (await store.load()).state.actions?.find((a) => a.kind === "follow" && same(a.handle, target.handle));
|
|
1682
|
-
if (!original)
|
|
1683
|
-
return {
|
|
1684
|
-
handle: target.handle,
|
|
1685
|
-
verified: false,
|
|
1686
|
-
status: "needsDecision",
|
|
1687
|
-
reason: "original-person-observation-required",
|
|
1688
|
-
};
|
|
1689
|
-
target = original;
|
|
1690
|
-
}
|
|
1691
|
-
const value = await readTask("readPerson", {
|
|
1692
|
-
...target,
|
|
1693
|
-
mode: "decision",
|
|
1694
|
-
});
|
|
1695
|
-
return {
|
|
1696
|
-
handle: target.handle,
|
|
1697
|
-
status: value.status,
|
|
1698
|
-
fresh: value.fresh,
|
|
1699
|
-
verified: value.status === "observed" &&
|
|
1700
|
-
!value.needsStateCheck &&
|
|
1701
|
-
!value.alerts?.length &&
|
|
1702
|
-
value.relationship === "following" &&
|
|
1703
|
-
(!target.accountHandle ||
|
|
1704
|
-
same(value.accountHandle, target.accountHandle)),
|
|
1705
|
-
...(value.evidenceRef ? { evidenceRef: value.evidenceRef } : {}),
|
|
1706
|
-
...(value.missing?.length ? { missing: value.missing } : {}),
|
|
1707
|
-
...(value.diagnostic
|
|
1708
|
-
? {
|
|
1709
|
-
diagnostic: {
|
|
1710
|
-
operation: value.diagnostic.operation,
|
|
1711
|
-
category: value.diagnostic.category,
|
|
1712
|
-
code: value.diagnostic.code,
|
|
1713
|
-
},
|
|
1714
|
-
}
|
|
1715
|
-
: {}),
|
|
1716
|
-
...(mode === "decision" ? { observation: value } : {}),
|
|
1717
|
-
};
|
|
1718
|
-
},
|
|
1719
|
-
runGrowthRound: (args) => executor ? parallelRound(args) : round(args),
|
|
1720
|
-
resumeGrowthRound: (args) => executor ? parallelRound(args) : round(args),
|
|
1721
|
-
inspectRound: async ({ taskId }) => {
|
|
1722
|
-
const snapshot = await load(taskId);
|
|
1723
|
-
return result(snapshot);
|
|
1724
|
-
},
|
|
1725
|
-
followOne: ({ taskId, handle }) => (executor ? parallelRound : round)({
|
|
1726
|
-
taskId,
|
|
1727
|
-
requestedKind: "follow",
|
|
1728
|
-
selector: (a) => a.kind === "follow" && same(a.handle, handle),
|
|
1729
|
-
}),
|
|
1730
|
-
followBatch: async ({ taskId, handles, preparedActions } = {}) => {
|
|
1731
|
-
if (!Array.isArray(handles) ||
|
|
1732
|
-
handles.length < 5 ||
|
|
1733
|
-
handles.length > 10 ||
|
|
1734
|
-
handles.some((h) => !/^[A-Za-z0-9_]{1,15}$/.test(h)) ||
|
|
1735
|
-
new Set(handles.map((h) => h.toLowerCase())).size !==
|
|
1736
|
-
handles.length)
|
|
1737
|
-
throw new Error("正常关注子批需5~10位不同的已准备对象");
|
|
1738
|
-
if (preparedActions?.some((a) => a.kind !== "follow" ||
|
|
1739
|
-
!handles.some((h) => same(h, a.handle))))
|
|
1740
|
-
throw new Error("batch-action-target-mismatch");
|
|
1741
|
-
await prepareActionsForCall(taskId, preparedActions);
|
|
1742
|
-
let batchId, resumedBatch = false;
|
|
1743
|
-
const original = await store.transaction(async (snapshot) => {
|
|
1744
|
-
if (!scope(snapshot.state).includes("follow"))
|
|
1745
|
-
throw new Error("kind-outside-round-scope");
|
|
1746
|
-
if (snapshot.state.batchId !== taskId ||
|
|
1747
|
-
handles.some((h) => !snapshot.state.actions?.some((a) => a.kind === "follow" && same(a.handle, h))))
|
|
1748
|
-
throw new Error("子批名单必须归原任务队列");
|
|
1749
|
-
const next = structuredClone(snapshot.state);
|
|
1750
|
-
next.followBatches ??= [];
|
|
1751
|
-
const previous = next.followBatches.findLast((b) => b.status !== "SETTLED" &&
|
|
1752
|
-
b.handles.length === handles.length &&
|
|
1753
|
-
b.handles.every((h, i) => same(h, handles[i])));
|
|
1754
|
-
resumedBatch = Boolean(previous);
|
|
1755
|
-
batchId = previous?.id ?? `${taskId}-FOLLOW-${stamp()}`;
|
|
1756
|
-
if (previous)
|
|
1757
|
-
Object.assign(previous, {
|
|
1758
|
-
status: "RUNNING",
|
|
1759
|
-
lastResumedAt: stamp(),
|
|
1760
|
-
});
|
|
1761
|
-
else
|
|
1762
|
-
next.followBatches.push({
|
|
1763
|
-
id: batchId,
|
|
1764
|
-
handles,
|
|
1765
|
-
planned: handles.length,
|
|
1766
|
-
startedAt: stamp(),
|
|
1767
|
-
status: "RUNNING",
|
|
1768
|
-
});
|
|
1769
|
-
next.updatedAt = stamp();
|
|
1770
|
-
return next;
|
|
1771
|
-
});
|
|
1772
|
-
const preparedCount = original.state.actions.filter((a) => a.kind === "follow" &&
|
|
1773
|
-
a.status === "prepared" &&
|
|
1774
|
-
handles.some((h) => same(h, a.handle))).length;
|
|
1775
|
-
let eligibleAt = clock();
|
|
1776
|
-
const previousGroup = original.state.execution?.followGroup;
|
|
1777
|
-
const lastAttempt = (original.state.attempts ?? [])
|
|
1778
|
-
.filter((a) => a.kind === "follow")
|
|
1779
|
-
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at))
|
|
1780
|
-
.at(-1);
|
|
1781
|
-
const lastAction = original.state.actions.find((a) => a.id === lastAttempt?.actionId);
|
|
1782
|
-
const sameBatchRecovery = resumedBatch &&
|
|
1783
|
-
lastAction?.status === "verified" &&
|
|
1784
|
-
evidenceExists(lastAction.resultEvidence) &&
|
|
1785
|
-
handles.some((h) => same(h, lastAction.handle));
|
|
1786
|
-
if (preparedCount && previousGroup?.attempts && !sameBatchRecovery)
|
|
1787
|
-
eligibleAt = Math.max(eligibleAt, Date.parse(previousGroup.lastAt) + 180000);
|
|
1788
|
-
for (const [duration, limit] of followWindows) {
|
|
1789
|
-
const recent = (original.state.attempts ?? [])
|
|
1790
|
-
.filter((a) => a.kind === "follow" && Date.parse(a.at) > clock() - duration)
|
|
1791
|
-
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
1792
|
-
if (preparedCount > limit - recent.length)
|
|
1793
|
-
eligibleAt = Math.max(eligibleAt, Date.parse(recent[recent.length - limit + preparedCount - 1].at) +
|
|
1794
|
-
duration);
|
|
1795
|
-
}
|
|
1796
|
-
if (eligibleAt > clock()) {
|
|
1797
|
-
const resumeAt = new Date(eligibleAt).toISOString();
|
|
1798
|
-
await store.transaction((snapshot) => {
|
|
1799
|
-
const next = structuredClone(snapshot.state);
|
|
1800
|
-
Object.assign(next.followBatches.find((b) => b.id === batchId), { status: "WAITING", remaining: preparedCount, resumeAt });
|
|
1801
|
-
next.updatedAt = stamp();
|
|
1802
|
-
return next;
|
|
1803
|
-
});
|
|
1804
|
-
return {
|
|
1805
|
-
taskId,
|
|
1806
|
-
batchId,
|
|
1807
|
-
planned: handles.length,
|
|
1808
|
-
attempted: 0,
|
|
1809
|
-
verifiedThisCall: 0,
|
|
1810
|
-
unknownInBatch: original.state.actions.filter((a) => handles.some((h) => same(h, a.handle)) &&
|
|
1811
|
-
["unknown", "intent"].includes(a.status)).length,
|
|
1812
|
-
remaining: preparedCount,
|
|
1813
|
-
mayEnd: false,
|
|
1814
|
-
decision: [
|
|
1815
|
-
{
|
|
1816
|
-
kind: "follow",
|
|
1817
|
-
status: "waiting",
|
|
1818
|
-
reason: "normal-batch-capacity-or-cooldown",
|
|
1819
|
-
resumeAt,
|
|
1820
|
-
},
|
|
1821
|
-
],
|
|
1822
|
-
evidenceRef: store.path,
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
let result;
|
|
1826
|
-
const exceptional = [];
|
|
1827
|
-
for (const handle of handles) {
|
|
1828
|
-
// A resumed batch submits only its remaining prepared members. Recovery
|
|
1829
|
-
// stays in the read-only API; a frozen member must not disturb that path.
|
|
1830
|
-
if (!original.state.actions.some((a) => a.kind === "follow" &&
|
|
1831
|
-
same(a.handle, handle) &&
|
|
1832
|
-
a.status === "prepared"))
|
|
1833
|
-
continue;
|
|
1834
|
-
result = await (executor ? parallelRound : round)({
|
|
1835
|
-
taskId,
|
|
1836
|
-
requestedKind: "follow",
|
|
1837
|
-
selector: (a) => a.kind === "follow" && same(a.handle, handle),
|
|
1838
|
-
});
|
|
1839
|
-
const relevant = (result.decision ?? []).filter((d) => d.kind === "follow" ||
|
|
1840
|
-
["harness-boundary-error", "original-ledger-conflict"].includes(d.reason) ||
|
|
1841
|
-
original.state.actions.some((a) => a.id === d.actionId && a.kind === "follow"));
|
|
1842
|
-
exceptional.push(...relevant);
|
|
1843
|
-
// A new unconfirmed submission is a diagnostic boundary, not permission
|
|
1844
|
-
// to consume the rest of the batch on the same failing write path.
|
|
1845
|
-
if (relevant.length)
|
|
1846
|
-
break;
|
|
1847
|
-
}
|
|
1848
|
-
const after = await store.load();
|
|
1849
|
-
const summary = {
|
|
1850
|
-
taskId,
|
|
1851
|
-
batchId,
|
|
1852
|
-
planned: handles.length,
|
|
1853
|
-
attempted: (after.state.attempts ?? [])
|
|
1854
|
-
.slice(original.state.attempts?.length ?? 0)
|
|
1855
|
-
.filter((a) => a.kind === "follow" &&
|
|
1856
|
-
handles.some((h) => same(after.state.actions.find((x) => x.id === a.actionId)
|
|
1857
|
-
?.handle, h))).length,
|
|
1858
|
-
verifiedThisCall: after.state.actions.filter((a) => a.kind === "follow" &&
|
|
1859
|
-
a.status === "verified" &&
|
|
1860
|
-
handles.some((h) => same(h, a.handle)) &&
|
|
1861
|
-
original.state.actions.find((old) => old.id === a.id)
|
|
1862
|
-
?.status !== "verified").length,
|
|
1863
|
-
unknownInBatch: after.state.actions.filter((a) => a.kind === "follow" &&
|
|
1864
|
-
handles.some((h) => same(a.handle, h)) &&
|
|
1865
|
-
["unknown", "intent"].includes(a.status)).length,
|
|
1866
|
-
remaining: after.state.actions.filter((a) => a.kind === "follow" &&
|
|
1867
|
-
handles.some((h) => same(a.handle, h)) &&
|
|
1868
|
-
a.status === "prepared").length,
|
|
1869
|
-
actual: result?.actual,
|
|
1870
|
-
decision: exceptional,
|
|
1871
|
-
mayEnd: result?.mayEnd ?? false,
|
|
1872
|
-
evidenceRef: store.path,
|
|
1873
|
-
};
|
|
1874
|
-
await store.transaction((snapshot) => {
|
|
1875
|
-
const next = structuredClone(snapshot.state), batch = next.followBatches.find((b) => b.id === batchId);
|
|
1876
|
-
Object.assign(batch, {
|
|
1877
|
-
attempted: (batch.attempted ?? 0) + summary.attempted,
|
|
1878
|
-
verifiedTotal: (batch.verifiedTotal ?? 0) + summary.verifiedThisCall,
|
|
1879
|
-
verifiedThisCall: summary.verifiedThisCall,
|
|
1880
|
-
unknown: summary.unknownInBatch,
|
|
1881
|
-
remaining: summary.remaining,
|
|
1882
|
-
finishedAt: stamp(),
|
|
1883
|
-
status: summary.remaining || summary.unknownInBatch ? "PARTIAL" : "SETTLED",
|
|
1884
|
-
});
|
|
1885
|
-
next.updatedAt = stamp();
|
|
1886
|
-
return next;
|
|
1887
|
-
});
|
|
1888
|
-
return summary;
|
|
1889
|
-
},
|
|
1890
|
-
publishPost: async (input) => {
|
|
1891
|
-
if (!executor && input.independentExecution)
|
|
1892
|
-
return publishIndependent(input);
|
|
1893
|
-
if (input.preparedAction &&
|
|
1894
|
-
(input.preparedAction.kind !== "original" ||
|
|
1895
|
-
input.preparedAction.draftPath !== input.draftPath))
|
|
1896
|
-
throw new Error("prepared-original-target-mismatch");
|
|
1897
|
-
await prepareActionsForCall(input.taskId, input.preparedAction ? [input.preparedAction] : []);
|
|
1898
|
-
return (executor ? parallelRound : round)({
|
|
1899
|
-
taskId: input.taskId,
|
|
1900
|
-
requestedKind: "original",
|
|
1901
|
-
selector: (a) => a.kind === "original" && a.draftPath === input.draftPath,
|
|
1902
|
-
});
|
|
1903
|
-
},
|
|
1904
|
-
replyToPost: async ({ taskId, postId, text, preparedAction }) => {
|
|
1905
|
-
if (preparedAction &&
|
|
1906
|
-
(preparedAction.kind !== "reply" ||
|
|
1907
|
-
preparedAction.postId !== postId ||
|
|
1908
|
-
preparedAction.text !== text))
|
|
1909
|
-
throw new Error("prepared-reply-target-mismatch");
|
|
1910
|
-
await prepareActionsForCall(taskId, preparedAction ? [preparedAction] : []);
|
|
1911
|
-
return (executor ? parallelRound : round)({
|
|
1912
|
-
taskId,
|
|
1913
|
-
requestedKind: "reply",
|
|
1914
|
-
selector: (a) => a.kind === "reply" && a.postId === postId && a.text === text,
|
|
1915
|
-
});
|
|
1916
|
-
},
|
|
1917
|
-
};
|
|
215
|
+
stamp() {
|
|
216
|
+
return new Date(this.#clock()).toISOString();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export function createGrowthHarness(...args) {
|
|
220
|
+
return new GrowthHarness(...args);
|
|
1918
221
|
}
|