From db5a4c7a4b61c46d0ddde5f0bdadf82bbc3d87bd Mon Sep 17 00:00:00 2001 From: Hleb Shauchenka Date: Sat, 20 Jun 2026 19:26:41 +0200 Subject: [PATCH 1/2] fix(slots): reconcile pinned flag on existing default-label slots seedDefaults (the slots default-label seeder) now reconciles the pinned flag on existing default-label slots to match the current template, while preserving user content. Pre-fix: default slots seeded before the current pinned=true defaults kept pinned=false forever and never reached SessionStart injection. Idempotent; user-written content untouched. Tests: test/slots.test.ts (+55 lines, 6 new cases). Signed-off-by: Hleb Shauchenka --- src/functions/slots.ts | 11 ++++++++- test/slots.test.ts | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/functions/slots.ts b/src/functions/slots.ts index 47b49d496..9a34cd8a7 100644 --- a/src/functions/slots.ts +++ b/src/functions/slots.ts @@ -159,7 +159,16 @@ async function seedDefaults(kv: StateKV): Promise { for (const tmpl of DEFAULT_SLOTS) { const target = scopeKv(tmpl.scope); const existing = await kv.get(target, tmpl.label); - if (existing) continue; + if (existing) { + if (existing.pinned !== tmpl.pinned) { + await kv.set(target, tmpl.label, { + ...existing, + pinned: tmpl.pinned, + updatedAt: ts, + }); + } + continue; + } const slot: MemorySlot = { ...tmpl, createdAt: ts, diff --git a/test/slots.test.ts b/test/slots.test.ts index 70da2aed1..9c9e7ad15 100644 --- a/test/slots.test.ts +++ b/test/slots.test.ts @@ -255,3 +255,59 @@ describe("slots — reflect", () => { expect(patterns.slot.content).toMatch(/errors: 2/); }); }); + + +describe("slots — seedDefaults reconciles pinned on existing default-label slots", () => { + function wireWith(kv: ReturnType) { + const handlers: Record) => Promise>> = {}; + const sdk = { + registerFunction: vi.fn((id: string, cb) => { + handlers[id] = cb; + }), + } as unknown as import("iii-sdk").ISdk; + registerSlotsFunctions(sdk, kv as never); + return handlers; + } + + it("re-pins a default-label slot that was stored with pinned:false, preserving content", async () => { + const kv = mockKV(); + await kv.set(KV.globalSlots, "persona", { + label: "persona", + content: "senior engineer persona", + sizeLimit: 1000, + description: "old description", + pinned: false, + readOnly: false, + scope: "global", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }); + wireWith(kv); + await waitForSeed(kv); + const persona = (await kv.get(KV.globalSlots, "persona")) as { + pinned: boolean; + content: string; + }; + expect(persona.pinned).toBe(true); + expect(persona.content).toBe("senior engineer persona"); + }); + + it("leaves a non-default custom slot untouched", async () => { + const kv = mockKV(); + await kv.set(KV.slots, "my_custom", { + label: "my_custom", + content: "custom", + sizeLimit: 2000, + description: "", + pinned: false, + readOnly: false, + scope: "project", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }); + wireWith(kv); + await waitForSeed(kv); + const custom = (await kv.get(KV.slots, "my_custom")) as { pinned: boolean }; + expect(custom.pinned).toBe(false); + }); +}); \ No newline at end of file From ecf66c21a5065e52792fdef20170d6ebd5af77a4 Mon Sep 17 00:00:00 2001 From: Hleb Shauchenka Date: Sat, 20 Jun 2026 22:23:57 +0200 Subject: [PATCH 2/2] fix(slots): audit pinned reconciliation writes seedDefaults now records a slot_seed_reconcile audit entry when it rewrites an existing default-label slot to flip its pinned flag, matching the project rule that every state-changing kv.set has an audit trail. --- src/functions/slots.ts | 5 +++++ src/types.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/functions/slots.ts b/src/functions/slots.ts index 9a34cd8a7..d1d49e999 100644 --- a/src/functions/slots.ts +++ b/src/functions/slots.ts @@ -166,6 +166,11 @@ async function seedDefaults(kv: StateKV): Promise { pinned: tmpl.pinned, updatedAt: ts, }); + await recordAudit(kv, "slot_seed_reconcile", "seedDefaults", [tmpl.label], { + scope: tmpl.scope, + fromPinned: existing.pinned, + toPinned: tmpl.pinned, + }); } continue; } diff --git a/src/types.ts b/src/types.ts index 6797dfaf9..e6506c045 100644 --- a/src/types.ts +++ b/src/types.ts @@ -608,7 +608,8 @@ export interface AuditEntry { | "slot_replace" | "slot_create" | "slot_delete" - | "slot_reflect"; + | "slot_reflect" + | "slot_seed_reconcile"; userId?: string; functionId: string; targetIds: string[];