@nextclaw/x-harness 0.2.58 → 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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Corresponding source
2
2
 
3
- Version 0.2.58, commit 4fa6559a68ec71e78c7d655732c4b570496f08b9.
3
+ Version 0.2.59, commit e0c324f79b77f50662ea2969c68ade82a31cb0b5.
4
4
 
5
- Complete source, build scripts and dependencies: https://github.com/Peiiii/x-harness/tree/4fa6559a68ec71e78c7d655732c4b570496f08b9
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";
@@ -329,6 +330,7 @@ export class BusinessHarness {
329
330
  if (["waiting", "needsCapability", "needsDecision"].includes(result.status))
330
331
  break;
331
332
  }
333
+ await this.recoverBatchReplyLinks(args.taskId, results);
332
334
  return {
333
335
  status: results.length === args.actions.length &&
334
336
  results.every((r) => ["verified", "preexisting"].includes(r.status))
@@ -337,6 +339,82 @@ export class BusinessHarness {
337
339
  results,
338
340
  };
339
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
+ }
340
418
  likeIncomingBatch(args) {
341
419
  return this.#reactions.run(args);
342
420
  }
@@ -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,6 @@
1
1
  {
2
2
  "name": "@nextclaw/x-harness",
3
- "version": "0.2.58",
3
+ "version": "0.2.59",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Typed X task harness with compact business results and explicit diagnostics.",
@@ -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; 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. Unknown results are never submitted again. 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.
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({