The OpenCode plugin's start path is per-process: it dedupes within one opencode run, but a restart or a resumed session fires kcap hook --opencode again for the same session id (OpenCodeHookCommand.LifecycleFor declares CallbackMayRepeat: true for exactly this reason). The memory index is fenced against the repeat by the durable once-per-session lease. The work-items nudge is not: WorkItemsNudgeEmitter.Resolve is called unconditionally at the output site (src/Capacitor.Cli/Commands/Harness/OpenCodeHookCommand.cs, the workItemsNudge assignment just before WriteMemoryFragment), gated only on the memory contract, so every restart or resume of the same session hands the plugin the nudge again.
Same shape as #668 (Antigravity, per turn) and the Kiro repeat fixed by #697 (per prompt). The OpenCode repeat is per restart/resume rather than per turn, so it accumulates one duplicate per restart instead of one per turn. That is why it was carved out of #697 rather than fixed there. Sibling: #785 (Pi, same shape); one NudgeLease wiring pass can close both.
Read from source, not reproduced against a live OpenCode. A repro is two invocations of the hook with the same session id and --memory-contract >= 1: the memory fragment appears once, the nudge appears both times.
Fix path
#697 introduced NudgeLease: a durable once-per-session claim riding SessionStartMemoryLeaseStore under a nudge-scoped key domain (SessionStartMemoryIdentity.CreateNudgeKey). It is harness-agnostic. NormalizeSessionId passes OpenCode ids through verbatim, so NudgeLease.TryClaimAsync(config, time, HarnessId.OpenCode, sessionId, budget) keys correctly today with no identity work.
Wire it the way KiroHookCommand does: start the claim beside the memory fetch, never let the fragment write wait on it, and refuse to emit on a repeat or an unavailable store. Kiro's deferred post-flush append exists because Kiro's ceiling is tight; check whether OpenCode's budget needs it or whether awaiting the claim bounded at the output site is enough.
The harness-setup nudge shares the call site but carries its own 6-hour evaluation throttle, which bounds it. Decide whether to fold it under the same claim (as #697 did for Kiro) or leave the throttle as the gate.
The OpenCode plugin's start path is per-process: it dedupes within one
opencoderun, but a restart or a resumed session fireskcap hook --opencodeagain for the same session id (OpenCodeHookCommand.LifecycleFordeclaresCallbackMayRepeat: truefor exactly this reason). The memory index is fenced against the repeat by the durable once-per-session lease. The work-items nudge is not:WorkItemsNudgeEmitter.Resolveis called unconditionally at the output site (src/Capacitor.Cli/Commands/Harness/OpenCodeHookCommand.cs, theworkItemsNudgeassignment just beforeWriteMemoryFragment), gated only on the memory contract, so every restart or resume of the same session hands the plugin the nudge again.Same shape as #668 (Antigravity, per turn) and the Kiro repeat fixed by #697 (per prompt). The OpenCode repeat is per restart/resume rather than per turn, so it accumulates one duplicate per restart instead of one per turn. That is why it was carved out of #697 rather than fixed there. Sibling: #785 (Pi, same shape); one
NudgeLeasewiring pass can close both.Read from source, not reproduced against a live OpenCode. A repro is two invocations of the hook with the same session id and
--memory-contract>= 1: the memory fragment appears once, the nudge appears both times.Fix path
#697 introduced
NudgeLease: a durable once-per-session claim ridingSessionStartMemoryLeaseStoreunder a nudge-scoped key domain (SessionStartMemoryIdentity.CreateNudgeKey). It is harness-agnostic.NormalizeSessionIdpasses OpenCode ids through verbatim, soNudgeLease.TryClaimAsync(config, time, HarnessId.OpenCode, sessionId, budget)keys correctly today with no identity work.Wire it the way
KiroHookCommanddoes: start the claim beside the memory fetch, never let the fragment write wait on it, and refuse to emit on a repeat or an unavailable store. Kiro's deferred post-flush append exists because Kiro's ceiling is tight; check whether OpenCode's budget needs it or whether awaiting the claim bounded at the output site is enough.The harness-setup nudge shares the call site but carries its own 6-hour evaluation throttle, which bounds it. Decide whether to fold it under the same claim (as #697 did for Kiro) or leave the throttle as the gate.