@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.
Files changed (113) hide show
  1. package/LICENSE +231 -6
  2. package/README.md +17 -3
  3. package/SOURCE.md +9 -0
  4. package/cli/main.js +232 -188
  5. package/dist/api.d.ts +56 -2
  6. package/dist/api.js +178 -75
  7. package/dist/boundary.js +1 -0
  8. package/dist/browser-common.d.ts +11 -0
  9. package/dist/browser-common.js +34 -18
  10. package/dist/browser-dom-capabilities.d.ts +9 -0
  11. package/dist/browser-dom-capabilities.js +131 -0
  12. package/dist/browser-dom-dependencies.d.ts +16 -0
  13. package/dist/browser-dom-dependencies.js +1 -0
  14. package/dist/browser-dom-editing.d.ts +166 -0
  15. package/dist/browser-dom-editing.js +418 -0
  16. package/dist/browser-dom-navigation.d.ts +18 -0
  17. package/dist/browser-dom-navigation.js +181 -0
  18. package/dist/browser-dom-network.d.ts +1 -0
  19. package/dist/browser-dom-network.js +128 -0
  20. package/dist/browser-dom-submission.d.ts +7 -0
  21. package/dist/browser-dom-submission.js +101 -0
  22. package/dist/browser-dom.d.ts +14 -2
  23. package/dist/browser-dom.js +120 -824
  24. package/dist/browser-native.d.ts +14 -1
  25. package/dist/browser-native.js +180 -143
  26. package/dist/browser.js +6 -2
  27. package/dist/creator-native.d.ts +3 -0
  28. package/dist/creator-native.js +113 -0
  29. package/dist/creator.js +1 -1
  30. package/dist/diagnostics.d.ts +11 -0
  31. package/dist/diagnostics.js +10 -0
  32. package/dist/growth-batch.d.ts +8 -0
  33. package/dist/growth-batch.js +190 -0
  34. package/dist/growth-constants.d.ts +5 -0
  35. package/dist/growth-constants.js +13 -0
  36. package/dist/growth-dependencies.d.ts +33 -0
  37. package/dist/growth-dependencies.js +1 -0
  38. package/dist/growth-execution.d.ts +17 -0
  39. package/dist/growth-execution.js +346 -0
  40. package/dist/growth-parallel.d.ts +8 -0
  41. package/dist/growth-parallel.js +332 -0
  42. package/dist/growth-plan.d.ts +9 -0
  43. package/dist/growth-plan.js +229 -0
  44. package/dist/growth-policy.d.ts +18 -0
  45. package/dist/growth-policy.js +208 -0
  46. package/dist/growth-publishing.d.ts +10 -0
  47. package/dist/growth-publishing.js +266 -0
  48. package/dist/growth-reading.d.ts +8 -0
  49. package/dist/growth-reading.js +157 -0
  50. package/dist/growth-recovery.d.ts +9 -0
  51. package/dist/growth-recovery.js +187 -0
  52. package/dist/growth-state.d.ts +16 -0
  53. package/dist/growth-state.js +220 -0
  54. package/dist/growth-tasks.d.ts +20 -0
  55. package/dist/growth-tasks.js +164 -0
  56. package/dist/growth.d.ts +26 -1
  57. package/dist/growth.js +211 -1908
  58. package/dist/platform-capabilities.d.ts +8 -0
  59. package/dist/platform-capabilities.js +29 -0
  60. package/dist/platform-collections.d.ts +14 -0
  61. package/dist/platform-collections.js +250 -0
  62. package/dist/platform-dependencies.d.ts +43 -0
  63. package/dist/platform-dependencies.js +1 -0
  64. package/dist/platform-diagnostics.d.ts +14 -0
  65. package/dist/platform-diagnostics.js +63 -0
  66. package/dist/platform-observation.d.ts +16 -0
  67. package/dist/platform-observation.js +96 -0
  68. package/dist/platform-rules.d.ts +17 -0
  69. package/dist/platform-rules.js +41 -0
  70. package/dist/platform-targets.d.ts +14 -0
  71. package/dist/platform-targets.js +154 -0
  72. package/dist/platform-writing.d.ts +16 -0
  73. package/dist/platform-writing.js +202 -0
  74. package/dist/platform.d.ts +23 -2
  75. package/dist/platform.js +125 -678
  76. package/dist/readers-actions.d.ts +2 -0
  77. package/dist/readers-actions.js +66 -0
  78. package/dist/readers-ax-host.d.ts +12 -0
  79. package/dist/readers-ax-host.js +134 -0
  80. package/dist/readers-ax-parser.d.ts +1 -0
  81. package/dist/readers-ax-parser.js +69 -0
  82. package/dist/readers-ax-program.d.ts +1 -0
  83. package/dist/readers-ax-program.js +5 -0
  84. package/dist/readers-ax-tree.d.ts +19 -0
  85. package/dist/readers-ax-tree.js +111 -0
  86. package/dist/readers-ax-views.d.ts +87 -0
  87. package/dist/readers-ax-views.js +247 -0
  88. package/dist/readers-cli.d.ts +6 -0
  89. package/dist/readers-cli.js +79 -0
  90. package/dist/readers-composer.d.ts +2 -0
  91. package/dist/readers-composer.js +131 -0
  92. package/dist/readers-navigation.d.ts +2 -0
  93. package/dist/readers-navigation.js +66 -0
  94. package/dist/readers-posts.d.ts +2 -0
  95. package/dist/readers-posts.js +141 -0
  96. package/dist/readers-profile.d.ts +3 -0
  97. package/dist/readers-profile.js +157 -0
  98. package/dist/readers.d.ts +9 -20
  99. package/dist/readers.js +9 -1045
  100. package/dist/results.js +1 -1
  101. package/dist/round-state.js +20 -3
  102. package/dist/store.d.ts +22 -4
  103. package/dist/store.js +173 -151
  104. package/dist/types.d.ts +10 -0
  105. package/dist/types.js +1 -0
  106. package/generated/observation.mjs +3 -2
  107. package/package.json +23 -9
  108. package/skill/SKILL.md +17 -36
  109. package/skill/references/api.md +7 -34
  110. package/skill/references/reads.md +23 -0
  111. package/skill/references/recovery.md +25 -0
  112. package/skill/references/setup.md +23 -0
  113. 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,5 @@
1
+ import type { BoundaryRecord } from "./boundary.js";
2
+ export declare const kinds: string[];
3
+ export declare const followWindows: number[][];
4
+ export declare const operations: BoundaryRecord;
5
+ export declare const same: (a: any, b: any) => boolean;
@@ -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;