Pi's kcap.ts extension re-fires session_start for the same session file on a restart (startup, reload) and on resume (PiHookCommand.LifecycleFor declares CallbackMayRepeat: true for exactly this reason). The memory index is fenced against the repeat by the durable once-per-session lease, keyed on the canonicalized session file path (PiSessionPathCanonicalizer), so a resume is deduped and a fork (new file) is freshly eligible. The work-items nudge is not: WorkItemsNudgeEmitter.Resolve is called unconditionally at the output site (src/Capacitor.Cli/Commands/Harness/PiHookCommand.cs, the workItemsNudge assignment just before WriteMemoryFragment), so every restart or resume of the same session hands the extension the nudge again.
Same shape as #668 (Antigravity, per turn) and the Kiro repeat fixed by #697 (per prompt). The Pi 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: #784 (OpenCode, same shape); one NudgeLease wiring pass can close both.
Read from source, not reproduced against a live Pi. A repro is two invocations of the hook for the same session file with --memory-contract >= 1, the second with reason=resume: 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, and NormalizeSessionId already routes HarnessId.Pi through PiSessionPathCanonicalizer, so a nudge claim inherits the lease's identity semantics for free: resume deduped, fork eligible. Pass the session file path to the claim, as the memory lease does, not the derived session id.
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 Pi'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.
Pi's
kcap.tsextension re-firessession_startfor the same session file on a restart (startup,reload) and onresume(PiHookCommand.LifecycleFordeclaresCallbackMayRepeat: truefor exactly this reason). The memory index is fenced against the repeat by the durable once-per-session lease, keyed on the canonicalized session file path (PiSessionPathCanonicalizer), so a resume is deduped and a fork (new file) is freshly eligible. The work-items nudge is not:WorkItemsNudgeEmitter.Resolveis called unconditionally at the output site (src/Capacitor.Cli/Commands/Harness/PiHookCommand.cs, theworkItemsNudgeassignment just beforeWriteMemoryFragment), so every restart or resume of the same session hands the extension the nudge again.Same shape as #668 (Antigravity, per turn) and the Kiro repeat fixed by #697 (per prompt). The Pi 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: #784 (OpenCode, same shape); one
NudgeLeasewiring pass can close both.Read from source, not reproduced against a live Pi. A repro is two invocations of the hook for the same session file with
--memory-contract>= 1, the second withreason=resume: 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, andNormalizeSessionIdalready routesHarnessId.PithroughPiSessionPathCanonicalizer, so a nudge claim inherits the lease's identity semantics for free: resume deduped, fork eligible. Pass the session file path to the claim, as the memory lease does, not the derived session id.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 Pi'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.