Retire duplicate reply candidates
This commit is contained in:
parent
0b624cb3b1
commit
ffbe3eb77d
3 changed files with 72 additions and 3 deletions
|
|
@ -419,9 +419,47 @@ function markCandidateInterventionSpoken(
|
|||
intervention.spokenAt = operation.receipt.sentAt;
|
||||
intervention.updatedAt = operation.receipt.sentAt;
|
||||
}
|
||||
retireSiblingCandidatesForReceipt(state.candidateInterventions, operation);
|
||||
state.candidateInterventions.updatedAt = operation.receipt.sentAt;
|
||||
}
|
||||
|
||||
function retireSiblingCandidatesForReceipt(
|
||||
candidates: VoidCandidateInterventions,
|
||||
operation: Extract<VoidSelfStateOperation, { operation: "mark_candidate_intervention_spoken" }>,
|
||||
): void {
|
||||
const { receipt } = operation;
|
||||
if (!receipt.replyToMessageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const intervention of candidates.interventions) {
|
||||
if (
|
||||
intervention.interventionId === operation.interventionId ||
|
||||
intervention.status !== "queued" ||
|
||||
intervention.spokenAt ||
|
||||
!candidateDeliveryTargetMatchesReceipt(intervention.deliveryTarget, receipt)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
intervention.status = "retired";
|
||||
intervention.retiredAt = receipt.sentAt;
|
||||
intervention.updatedAt = receipt.sentAt;
|
||||
intervention.tags = Array.from(new Set([...intervention.tags, "retired:duplicate reply target already answered"]));
|
||||
}
|
||||
}
|
||||
|
||||
function candidateDeliveryTargetMatchesReceipt(
|
||||
deliveryTarget: VoidCandidateInterventions["interventions"][number]["deliveryTarget"],
|
||||
receipt: VoidSpeechReceipts["recentReceipts"][number],
|
||||
): boolean {
|
||||
return (
|
||||
deliveryTarget?.mode === "channel" &&
|
||||
deliveryTarget.channelId === receipt.channelId &&
|
||||
deliveryTarget.replyToMessageId === receipt.replyToMessageId
|
||||
);
|
||||
}
|
||||
|
||||
function upsertBy<T>(entries: T[], value: T, keyOf: (value: T) => string): void {
|
||||
const key = keyOf(value);
|
||||
const index = entries.findIndex((entry) => keyOf(entry) === key);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,32 @@ process.stdin.on("end", () => {
|
|||
tags: ["fixture"],
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-duplicate-same-reply-target",
|
||||
kind: "direct_reply",
|
||||
status: "queued",
|
||||
target: {
|
||||
kind: "room",
|
||||
id: "fixture-channel",
|
||||
label: "Fixture channel",
|
||||
},
|
||||
summary: "Duplicate same-target candidate should retire when the sibling speaks.",
|
||||
draft: "This duplicate should never be delivered after the same reply target is answered.",
|
||||
deliveryTarget: {
|
||||
mode: "channel",
|
||||
channelId: "fixture-channel-id",
|
||||
replyToMessageId: "fixture-message-id",
|
||||
personaName: "Void",
|
||||
},
|
||||
priority: 0.7,
|
||||
mustEventuallyShare: false,
|
||||
createdAt: "2026-05-17T03:00:01.000Z",
|
||||
updatedAt: "2026-05-17T03:00:01.000Z",
|
||||
tags: ["fixture", "duplicate"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
|
|
@ -154,18 +180,22 @@ process.stdout.write(JSON.stringify({ ok: true, mode: "channel", channelId }) +
|
|||
if ($status.status -ne "ok") {
|
||||
throw "Speech fixture did not finish ok."
|
||||
}
|
||||
if ([int]$status.proposedOperationCount -ne 1 -or [int]$status.appliedOperationCount -ne 2 -or [int]$status.deliveredCandidateCount -ne 1) {
|
||||
throw "Speech fixture expected one proposal, two applied operations, and one delivered candidate."
|
||||
if ([int]$status.proposedOperationCount -ne 2 -or [int]$status.appliedOperationCount -ne 3 -or [int]$status.deliveredCandidateCount -ne 1) {
|
||||
throw "Speech fixture expected two proposals, three applied operations, and one delivered candidate."
|
||||
}
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$candidate = @($state.candidateInterventions.interventions)[0]
|
||||
$candidate = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-parent-owned-speech" })[0]
|
||||
$duplicateCandidate = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-duplicate-same-reply-target" })[0]
|
||||
$receipt = @($state.speechReceipts.recentReceipts)[0]
|
||||
|
||||
if ($candidate.status -ne "spoken") {
|
||||
throw "Speech fixture did not mark the candidate spoken."
|
||||
}
|
||||
if ($duplicateCandidate.status -ne "retired") {
|
||||
throw "Speech fixture did not retire the duplicate same-target candidate."
|
||||
}
|
||||
if ($receipt.channelId -ne "fixture-channel-id" -or $receipt.replyToMessageId -ne "fixture-message-id") {
|
||||
throw "Speech fixture did not preserve the delivery receipt target."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,3 +127,4 @@
|
|||
{"ts":"2026-05-17T19:15:56Z","type":"implementation","status":"ok","note":"Live-loop inspection complaints resolved: candidate speech receipts now link back to the spoken intervention, rumination report text is refreshed by the runner, watchdog Repixelizer SSH checks no longer hang indefinitely, and offsite sync recovered with a fresh remote archive."}
|
||||
{"ts":"2026-05-17T22:41:56Z","type":"implementation","status":"ok","note":"Persona webhook delivery now supports configured per-channel webhook URLs and preserves Discord failure details in the parent rumination runner; live probe confirms current #general blocker is Discord 403 Missing Permissions unless Manage Webhooks is granted or DISCORD_PERSONA_WEBHOOK_URL_113786069023064068 is configured."}
|
||||
{"ts":"2026-05-17T23:50:11Z","type":"implementation","status":"ok","note":"Persona webhook delivery now resolves avatar URLs from per-persona env keys such as DISCORD_PERSONA_AVATAR_URL_VOID, falling back to DISCORD_PERSONA_AVATAR_URL; live Void probe used the bot's Discord CDN avatar without an explicit operation-level avatar URL."}
|
||||
{"ts":"2026-05-17T23:59:13Z","type":"implementation","status":"ok","note":"Repeated logo reply at 2026-05-17T23:56Z was caused by two queued candidates sharing the same Discord reply target after webhook delivery surgery. Marking one candidate spoken now retires queued sibling candidates for the same channel/reply target; speech fixture covers the duplicate-target case."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue