@nextclaw/x-harness 0.2.57 → 0.2.59
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/SOURCE.md +2 -2
- package/dist/api.d.ts +1 -0
- package/dist/api.js +97 -5
- package/dist/browser-dom-submission.d.ts +1 -0
- package/dist/browser-dom-submission.js +33 -2
- package/dist/growth-execution.js +1 -4
- package/dist/platform-rules.d.ts +1 -0
- package/dist/platform-rules.js +1 -0
- package/dist/results.js +11 -0
- package/dist/round-state.js +33 -0
- package/package.json +26 -3
- package/skill/references/reads.md +3 -1
- package/skill/references/writes.md +1 -1
package/SOURCE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Corresponding source
|
|
2
2
|
|
|
3
|
-
Version 0.2.
|
|
3
|
+
Version 0.2.59, commit e0c324f79b77f50662ea2969c68ade82a31cb0b5.
|
|
4
4
|
|
|
5
|
-
Complete source, build scripts and dependencies: https://github.com/Peiiii/x-harness/tree/
|
|
5
|
+
Complete source, build scripts and dependencies: https://github.com/Peiiii/x-harness/tree/e0c324f79b77f50662ea2969c68ade82a31cb0b5
|
|
6
6
|
|
|
7
7
|
Clone this revision, run npm ci, npm run bootstrap and npm run release:build.
|
|
8
8
|
|
package/dist/api.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class BusinessHarness implements XGrowthHarness {
|
|
|
26
26
|
results: ActionResult[];
|
|
27
27
|
reason?: undefined;
|
|
28
28
|
}>;
|
|
29
|
+
private recoverBatchReplyLinks;
|
|
29
30
|
likeIncomingBatch(args: import("./types.js").IncomingLikeOptions): Promise<import("./types.js").IncomingLikeResult>;
|
|
30
31
|
readCreator(args: import("./types.js").CreatorOptions): Promise<ReadResult>;
|
|
31
32
|
readBookmarks(args: import("./types.js").BookmarkOptions): Promise<ReadResult>;
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sameHandle, samePublishedText } from "./platform-rules.js";
|
|
1
2
|
import { IncomingReactions } from "./incoming-reactions.js";
|
|
2
3
|
import { createGrowthHarness } from "./growth.js";
|
|
3
4
|
import { readResult, decisions, diagnostic, stableReason } from "./results.js";
|
|
@@ -7,6 +8,7 @@ export class BusinessHarness {
|
|
|
7
8
|
#store;
|
|
8
9
|
#core;
|
|
9
10
|
#readCache;
|
|
11
|
+
#clock;
|
|
10
12
|
constructor(dependencies) {
|
|
11
13
|
this.replyBatch = this.replyBatch.bind(this);
|
|
12
14
|
this.likeIncomingBatch = this.likeIncomingBatch.bind(this);
|
|
@@ -31,6 +33,7 @@ export class BusinessHarness {
|
|
|
31
33
|
this.readBookmarks = this.readBookmarks.bind(this);
|
|
32
34
|
this.readCreator = this.readCreator.bind(this);
|
|
33
35
|
const { store, platform } = dependencies;
|
|
36
|
+
this.#clock = dependencies.clock ?? Date.now;
|
|
34
37
|
this.#platform = platform;
|
|
35
38
|
this.#store = store;
|
|
36
39
|
this.#core = createGrowthHarness(dependencies);
|
|
@@ -303,18 +306,31 @@ export class BusinessHarness {
|
|
|
303
306
|
text: action.text,
|
|
304
307
|
preparedAction: action,
|
|
305
308
|
});
|
|
306
|
-
if (result.status === "
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
if (result.status === "waiting" &&
|
|
310
|
+
result.reason === "incoming-interaction-pacing" &&
|
|
311
|
+
result.resumeAt) {
|
|
312
|
+
const delay = Date.parse(result.resumeAt) - this.#clock();
|
|
313
|
+
if (Number.isFinite(delay) && delay <= 30000) {
|
|
314
|
+
if (delay > 0)
|
|
315
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
316
|
+
result = await this.replyToPost({
|
|
310
317
|
taskId: args.taskId,
|
|
311
|
-
|
|
318
|
+
postId: action.postId,
|
|
319
|
+
text: action.text,
|
|
320
|
+
preparedAction: action,
|
|
312
321
|
});
|
|
322
|
+
}
|
|
313
323
|
}
|
|
324
|
+
if (result.status === "unknown")
|
|
325
|
+
result = await this.verifySubmission({
|
|
326
|
+
taskId: args.taskId,
|
|
327
|
+
actionId: action.id,
|
|
328
|
+
});
|
|
314
329
|
results.push(result);
|
|
315
330
|
if (["waiting", "needsCapability", "needsDecision"].includes(result.status))
|
|
316
331
|
break;
|
|
317
332
|
}
|
|
333
|
+
await this.recoverBatchReplyLinks(args.taskId, results);
|
|
318
334
|
return {
|
|
319
335
|
status: results.length === args.actions.length &&
|
|
320
336
|
results.every((r) => ["verified", "preexisting"].includes(r.status))
|
|
@@ -323,6 +339,82 @@ export class BusinessHarness {
|
|
|
323
339
|
results,
|
|
324
340
|
};
|
|
325
341
|
}
|
|
342
|
+
async recoverBatchReplyLinks(taskId, results) {
|
|
343
|
+
const snapshot = await this.#store.load();
|
|
344
|
+
if (snapshot.state.batchId !== taskId)
|
|
345
|
+
return;
|
|
346
|
+
const pending = results
|
|
347
|
+
.filter((r) => r.status === "unknown")
|
|
348
|
+
.map((r) => snapshot.state.actions.find((a) => a.id === r.actionId))
|
|
349
|
+
.filter((a) => a?.kind === "reply" &&
|
|
350
|
+
["unknown", "intent"].includes(a.status) &&
|
|
351
|
+
!a.observedPublicURL &&
|
|
352
|
+
typeof a.text === "string" &&
|
|
353
|
+
a.text.trim() &&
|
|
354
|
+
/^[A-Za-z0-9_]{1,15}$/.test(a.accountHandle ?? "") &&
|
|
355
|
+
Number.isFinite(Date.parse(a.intentAt)));
|
|
356
|
+
if (!pending.length ||
|
|
357
|
+
!pending.every((a) => sameHandle(a.accountHandle, pending[0].accountHandle)))
|
|
358
|
+
return;
|
|
359
|
+
const phrases = pending
|
|
360
|
+
.map((a) => '"' + a.text.slice(0, 80).replace(/["\\]/g, " ") + '"')
|
|
361
|
+
.join(" OR ");
|
|
362
|
+
const query = `from:${pending[0].accountHandle} (${phrases}) since:${new Date(Math.min(...pending.map((a) => Date.parse(a.intentAt)))).toISOString().slice(0, 10)}`;
|
|
363
|
+
let found = await this.searchSources({
|
|
364
|
+
query,
|
|
365
|
+
limit: 5,
|
|
366
|
+
maxPages: 1,
|
|
367
|
+
mode: "decision",
|
|
368
|
+
});
|
|
369
|
+
if (!found.sources?.length &&
|
|
370
|
+
found.missing?.includes("source-page-unconfirmed"))
|
|
371
|
+
found = await this.searchSources({
|
|
372
|
+
query,
|
|
373
|
+
limit: 5,
|
|
374
|
+
maxPages: 1,
|
|
375
|
+
mode: "decision",
|
|
376
|
+
});
|
|
377
|
+
if (!found.fresh || !found.evidenceRef)
|
|
378
|
+
return;
|
|
379
|
+
for (const action of pending) {
|
|
380
|
+
const matches = (found.sources ?? []).filter((p) => sameHandle(p.author, action.accountHandle) &&
|
|
381
|
+
samePublishedText(p.body, action.text) &&
|
|
382
|
+
/^\d+$/.test(p.id ?? "") &&
|
|
383
|
+
!p.needsExpansion &&
|
|
384
|
+
p.url?.toLowerCase() ===
|
|
385
|
+
`https://x.com/${action.accountHandle}/status/${p.id}`.toLowerCase() &&
|
|
386
|
+
!(action.beforeIds ?? []).includes(p.id) &&
|
|
387
|
+
Date.parse(p.datetime ?? "") >= Date.parse(action.intentAt) &&
|
|
388
|
+
Date.parse(p.datetime ?? "") <= this.#clock() + 5000);
|
|
389
|
+
if (matches.length !== 1)
|
|
390
|
+
continue;
|
|
391
|
+
let bound = false;
|
|
392
|
+
await this.#store.transaction(({ state }) => {
|
|
393
|
+
const current = state.actions.find((a) => a.id === action.id);
|
|
394
|
+
if (state.batchId === taskId &&
|
|
395
|
+
current?.status === "unknown" &&
|
|
396
|
+
current.intentAt === action.intentAt &&
|
|
397
|
+
current.postId === action.postId &&
|
|
398
|
+
current.text === action.text &&
|
|
399
|
+
!current.observedPublicURL &&
|
|
400
|
+
!current.claim) {
|
|
401
|
+
current.observedPublicURL = matches[0].url;
|
|
402
|
+
current.observedPublicEvidenceRef = found.evidenceRef;
|
|
403
|
+
bound = true;
|
|
404
|
+
}
|
|
405
|
+
return state;
|
|
406
|
+
});
|
|
407
|
+
if (bound) {
|
|
408
|
+
const recovered = await this.verifySubmission({
|
|
409
|
+
taskId,
|
|
410
|
+
actionId: action.id,
|
|
411
|
+
});
|
|
412
|
+
const index = results.findIndex((r) => r.actionId === action.id);
|
|
413
|
+
if (index >= 0)
|
|
414
|
+
results[index] = recovered;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
326
418
|
likeIncomingBatch(args) {
|
|
327
419
|
return this.#reactions.run(args);
|
|
328
420
|
}
|
|
@@ -3,5 +3,6 @@ export declare class DOMSubmission {
|
|
|
3
3
|
#private;
|
|
4
4
|
constructor(dependencies: Pick<DOMDependencies, "submitAX" | "session" | "tab" | "observe" | "unique" | "locator">);
|
|
5
5
|
finishDOMAction(command: any, prepared: any, query: any, result: any): Promise<void>;
|
|
6
|
+
private waitSubmittedEditor;
|
|
6
7
|
}
|
|
7
8
|
export declare function createDOMSubmission(...args: ConstructorParameters<typeof DOMSubmission>): (command: any, prepared: any, query: any, result: any) => Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { xURL } from "./platform-rules.js";
|
|
1
|
+
import { xURL, sameHandle, sameText } from "./platform-rules.js";
|
|
2
2
|
import { valueOf } from "./browser-common.js";
|
|
3
3
|
export class DOMSubmission {
|
|
4
4
|
#submitAX;
|
|
@@ -29,6 +29,7 @@ export class DOMSubmission {
|
|
|
29
29
|
: null;
|
|
30
30
|
let nativeSubmitIndex;
|
|
31
31
|
if (this.#submitAX &&
|
|
32
|
+
command.activation !== "keyboard" &&
|
|
32
33
|
["submitPost", "submitReply"].includes(command.operation)) {
|
|
33
34
|
const native = await this.#submitAX.observe({ mode: "composer" });
|
|
34
35
|
const editors = native.evidence?.filter((e) => /^(?:文本输入区(?:域)?|文本区域|文本栏) /.test(e.label)) ?? [];
|
|
@@ -88,7 +89,7 @@ export class DOMSubmission {
|
|
|
88
89
|
// Inline replies keep the editor mounted. Wait for this submitted text to clear before
|
|
89
90
|
// receipt navigation/reload can interrupt the actual submission request.
|
|
90
91
|
if (submittedEditor)
|
|
91
|
-
await
|
|
92
|
+
await this.waitSubmittedEditor(submittedEditor, result);
|
|
92
93
|
if (["submitPost", "submitReply"].includes(command.operation)) {
|
|
93
94
|
try {
|
|
94
95
|
await this.#tab.playwright.waitForLoadState({
|
|
@@ -104,6 +105,36 @@ export class DOMSubmission {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
async waitSubmittedEditor(editor, submitted) {
|
|
109
|
+
const deadline = Date.now() + 10000;
|
|
110
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
111
|
+
const remaining = deadline - Date.now();
|
|
112
|
+
if (remaining <= 0)
|
|
113
|
+
throw Error("submitted-editor-clear-timeout");
|
|
114
|
+
try {
|
|
115
|
+
await editor.waitFor({ state: "hidden", timeoutMs: remaining });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!/timed out|timeout|deadline exceeded/i.test(error.message) ||
|
|
120
|
+
attempt === 3 ||
|
|
121
|
+
Date.now() >= deadline)
|
|
122
|
+
throw error;
|
|
123
|
+
// Resume only the wait, never the submission or a navigation that could interrupt it.
|
|
124
|
+
const current = await this.#observe({ view: "composer" });
|
|
125
|
+
if (current.alerts?.length ||
|
|
126
|
+
!current.accountHandle ||
|
|
127
|
+
!submitted.accountHandle ||
|
|
128
|
+
!sameHandle(current.accountHandle, submitted.accountHandle) ||
|
|
129
|
+
xURL(current.page).href !== xURL(submitted.page).href ||
|
|
130
|
+
(typeof current.editorText === "string" &&
|
|
131
|
+
current.editorText.trim() &&
|
|
132
|
+
(!sameText(current.editorText, submitted.editorText) ||
|
|
133
|
+
current.replyToId !== submitted.replyToId)))
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
107
138
|
}
|
|
108
139
|
export function createDOMSubmission(...args) {
|
|
109
140
|
return new DOMSubmission(...args).finishDOMAction;
|
package/dist/growth-execution.js
CHANGED
|
@@ -170,10 +170,7 @@ export class GrowthExecution {
|
|
|
170
170
|
if (action.kind !== "follow") {
|
|
171
171
|
let baseline;
|
|
172
172
|
try {
|
|
173
|
-
baseline = await this.#platform.findSubmission({
|
|
174
|
-
...action,
|
|
175
|
-
intentAt: this.#stamp(),
|
|
176
|
-
});
|
|
173
|
+
baseline = await this.#platform.findSubmission({ ...action, intentAt: this.#stamp() }, { phase: "baseline" });
|
|
177
174
|
}
|
|
178
175
|
catch (error) {
|
|
179
176
|
return {
|
package/dist/platform-rules.d.ts
CHANGED
package/dist/platform-rules.js
CHANGED
package/dist/results.js
CHANGED
|
@@ -46,6 +46,17 @@ export function source(value, page) {
|
|
|
46
46
|
/* explicit missing below */
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
if (raw.metrics !== undefined)
|
|
50
|
+
out.metrics = [
|
|
51
|
+
...new Set(list(raw.metrics).flatMap((m) => {
|
|
52
|
+
const label = typeof m === "string" ? m : object(m).label;
|
|
53
|
+
return typeof label === "string" &&
|
|
54
|
+
/\d/.test(label) &&
|
|
55
|
+
/回复|转帖|喜欢|书签|观看|查看|replies|reposts?|likes?|bookmarks?|views?/i.test(label)
|
|
56
|
+
? [label]
|
|
57
|
+
: [];
|
|
58
|
+
})),
|
|
59
|
+
];
|
|
49
60
|
if (raw.quoteCards)
|
|
50
61
|
out.quoteCards = list(raw.quoteCards).map((v) => pick(v, ["text", "datetime", "body"]));
|
|
51
62
|
if (raw.quoteSource)
|
package/dist/round-state.js
CHANGED
|
@@ -157,6 +157,39 @@ export function runRoundCLI() {
|
|
|
157
157
|
scopeEvidenceRef: execution.evidenceRef,
|
|
158
158
|
stop: execution.stop,
|
|
159
159
|
};
|
|
160
|
+
const gap = execution.followGapCondition;
|
|
161
|
+
if (gap &&
|
|
162
|
+
execution.scopeKinds.length === 1 &&
|
|
163
|
+
execution.scopeKinds[0] === "reply") {
|
|
164
|
+
const ids = gap.actionIds;
|
|
165
|
+
if (!gap.sourceExecutor ||
|
|
166
|
+
!existsSync(gap.authorizationEvidenceRef ?? "") ||
|
|
167
|
+
!Array.isArray(ids) ||
|
|
168
|
+
!ids.length ||
|
|
169
|
+
new Set(ids).size !== ids.length ||
|
|
170
|
+
gap.planned !== ids.length)
|
|
171
|
+
throw Error("conditional-reply-scope-unconfirmed");
|
|
172
|
+
const actions = ids.map((id) => state.actions.find((a) => a.id === id));
|
|
173
|
+
if (actions.some((a) => !a || a.kind !== "reply"))
|
|
174
|
+
throw Error("conditional-reply-actions-unconfirmed");
|
|
175
|
+
scoped.items = state.items.map((item) => item.kind !== "reply"
|
|
176
|
+
? item
|
|
177
|
+
: {
|
|
178
|
+
...state.items.find((i) => i.kind === "reply"),
|
|
179
|
+
planned: ids.length,
|
|
180
|
+
verified: actions.filter((a) => a.status === "verified")
|
|
181
|
+
.length,
|
|
182
|
+
unknown: actions.filter((a) => ["intent", "unknown"].includes(a.status)).length,
|
|
183
|
+
actionable: actions.some((a) => a.status === "prepared"),
|
|
184
|
+
reason: execution.currentWork,
|
|
185
|
+
resumeAt: execution.stop?.nextEligibleCheckAt,
|
|
186
|
+
resumeWhen: execution.currentWork,
|
|
187
|
+
blockEvidence: [execution.stop?.evidenceRef].filter(Boolean),
|
|
188
|
+
resultEvidence: actions
|
|
189
|
+
.filter((a) => a.status === "verified")
|
|
190
|
+
.map((a) => a.resultEvidence),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
160
193
|
}
|
|
161
194
|
const result = checkRound(scoped);
|
|
162
195
|
console.log(JSON.stringify(result));
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/x-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.59",
|
|
4
|
+
"private": false,
|
|
4
5
|
"type": "module",
|
|
5
6
|
"description": "Typed X task harness with compact business results and explicit diagnostics.",
|
|
6
7
|
"main": "./dist/index.js",
|
|
@@ -24,6 +25,29 @@
|
|
|
24
25
|
"LICENSE",
|
|
25
26
|
"SOURCE.md"
|
|
26
27
|
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "npm run maintainability && tsc -p tsconfig.json && node scripts/generate-observation.mjs",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"test": "node --test test/*.test.mjs",
|
|
32
|
+
"check": "npm run maintainability && npm run format:check && npm run typecheck && npm run typecheck:cli && npm run build && npm run typecheck:contracts && npm run test && node scripts/check-skill.mjs && node scripts/generate-observation.mjs --check",
|
|
33
|
+
"prepack": "node release/build.mjs --root-pack",
|
|
34
|
+
"typecheck:contracts": "tsc -p tsconfig.contracts.json",
|
|
35
|
+
"format:check": "prettier --check src test scripts release site",
|
|
36
|
+
"maintainability": "node scripts/check-maintainability.mjs",
|
|
37
|
+
"release:build": "node release/build.mjs",
|
|
38
|
+
"test:release": "node --test release/test/*.test.mjs",
|
|
39
|
+
"site:build": "node site/build.mjs",
|
|
40
|
+
"site:deploy": "node site/deploy.mjs",
|
|
41
|
+
"bootstrap": "node scripts/bootstrap.mjs",
|
|
42
|
+
"ai:check": "qingtian check --target .",
|
|
43
|
+
"typecheck:cli": "tsc -p release/tsconfig.json --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "22",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"prettier": "^3.9.9",
|
|
49
|
+
"@nextclaw/qingtian": "0.1.3"
|
|
50
|
+
},
|
|
27
51
|
"license": "AGPL-3.0-only",
|
|
28
52
|
"homepage": "https://x-harness.gemigo.app/",
|
|
29
53
|
"repository": {
|
|
@@ -41,6 +65,5 @@
|
|
|
41
65
|
},
|
|
42
66
|
"publishConfig": {
|
|
43
67
|
"access": "public"
|
|
44
|
-
}
|
|
45
|
-
"gitHead": "b9b2f4172170380da329027f31a6526062e41218"
|
|
68
|
+
}
|
|
46
69
|
}
|
|
@@ -21,10 +21,12 @@ observed ≠ field completeness; pageComplete:false = scope. View media before d
|
|
|
21
21
|
|
|
22
22
|
`readIncomingReplies`: ledger account; skip settled/intent/unknown and processedIds across dates. readPost(includeContext:true) for ancestors; missing context blocks review. Dedup publicly.
|
|
23
23
|
|
|
24
|
-
Incoming: `readIncomingConversations({targets?,...window})` combines discovery/context (≤5); no preceding `readIncomingReplies`. Preflight/source failures remain visible. Returns ancestorPath, nodes, confirmed edges, unknown parents and attempts; rootCandidateId is only the earliest loaded ancestor. Missing context blocks review; never guess roots/edges or inbox clearance.
|
|
24
|
+
Incoming: `readIncomingConversations({targets?,...window})` combines discovery/context (≤5); no preceding `readIncomingReplies`. Preflight/source failures remain visible. Returns ancestorPath, nodes, confirmed edges, unknown parents and attempts; rootCandidateId is only the earliest loaded ancestor. Missing context blocks review; never guess roots/edges or inbox clearance.
|
|
25
25
|
|
|
26
26
|
`resolveQuote:true` follows one observed card and returns `post.quoteSource` only after exact destination/body/author/time binding.
|
|
27
27
|
|
|
28
28
|
`readPeople`: observed follower/following; offset reuses cells; advance scrolls a fresh unique handle. Full bio/blue/relationship; dedup; unknowns need profiles. No Native.
|
|
29
29
|
|
|
30
30
|
Discovery: names only.
|
|
31
|
+
|
|
32
|
+
Source decisions retain visible metric labels for batch screening; absent counts stay unknown.
|
|
@@ -49,7 +49,7 @@ Optional independentExecution:{tabId,evidenceRef,scopeEvidenceRef} requires real
|
|
|
49
49
|
|
|
50
50
|
## Reply
|
|
51
51
|
|
|
52
|
-
Additional fields: kind:'reply', postId, url, evidenceRef (observed parent), text; optional assets/activation as above. Common account/quality fields still required. The exact parent is verified by the API. `replyBatch({taskId,actions})` accepts 1–5 reviewed replies, submits serially and recovers an observed receipt once without resending;
|
|
52
|
+
Additional fields: kind:'reply', postId, url, evidenceRef (observed parent), text; optional assets/activation as above. Common account/quality fields still required. The exact parent is verified by the API. `replyBatch({taskId,actions})` accepts 1–5 reviewed replies, submits serially and recovers an observed receipt once without resending; The batch owns one normal pacing wait (up to 30 seconds) per action and one read-only verification for an unknown result, including when no permalink was captured. Longer or renewed waits and capability/decision failures return unfinished work. Remaining unknowns without permalinks get one batched own-account lookup, then exact-parent verification of unique observed links; unknowns never resubmit. Incoming clicks have a ≥30s minimum gap, not a safety guarantee. Incoming conversations require reviewed conversation:{recipientHandle,rootId,round,automatedLoop?}; at most 3 own rounds per recipient/root, with intent/unknown counted. Mark observed automatic loops to stop earlier.
|
|
53
53
|
|
|
54
54
|
```js
|
|
55
55
|
var result = await x.replyToPost({
|