Skip to content

Commit 160350e

Browse files
mason: fix postprocess reclaim and replay gates
Co-authored-by: Alfonso [Magic Context] <288211368+alfonso-magic-context@users.noreply.github.com>
1 parent 8da04e9 commit 160350e

13 files changed

Lines changed: 352 additions & 86 deletions

packages/pi-plugin/src/ctx-reduce-nudge-pi.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ describe("maybeDeliverChannel2Pi", () => {
315315
const statement = originalPrepare(sql);
316316
if (
317317
sql ===
318-
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ? WHERE session_id = ? AND channel2_nudge_state = ?"
318+
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = '' WHERE session_id = ? AND channel2_nudge_state = ?"
319319
) {
320320
return {
321321
...statement,

packages/plugin/src/features/magic-context/storage-db.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
834834
last_nudge_level TEXT DEFAULT '',
835835
channel2_nudge_state TEXT DEFAULT '',
836836
channel2_nudge_claimed_at INTEGER DEFAULT 0,
837+
channel2_nudge_claim_token TEXT DEFAULT '',
837838
last_emergency_input_sample INTEGER DEFAULT 0,
838839
last_transform_error TEXT DEFAULT '',
839840
nudge_anchor_message_id TEXT DEFAULT '',
@@ -1082,6 +1083,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
10821083
ensureColumn(db, "session_meta", "last_nudge_level", "TEXT DEFAULT ''");
10831084
ensureColumn(db, "session_meta", "channel2_nudge_state", "TEXT DEFAULT ''");
10841085
ensureColumn(db, "session_meta", "channel2_nudge_claimed_at", "INTEGER DEFAULT 0");
1086+
ensureColumn(db, "session_meta", "channel2_nudge_claim_token", "TEXT DEFAULT ''");
10851087
ensureColumn(db, "session_meta", "last_emergency_input_sample", "INTEGER DEFAULT 0");
10861088
ensureColumn(db, "session_meta", "last_transform_error", "TEXT DEFAULT ''");
10871089
ensureColumn(db, "session_meta", "nudge_anchor_message_id", "TEXT DEFAULT ''");
@@ -1451,7 +1453,7 @@ function healWedgedChannel2Claims(db: Database): void {
14511453
try {
14521454
const staleBefore = Date.now() - CHANNEL2_CLAIM_TTL_MS;
14531455
db.prepare(
1454-
"UPDATE session_meta SET channel2_nudge_state = 'pending', channel2_nudge_claimed_at = 0 WHERE channel2_nudge_state = 'claimed' AND (channel2_nudge_claimed_at IS NULL OR channel2_nudge_claimed_at = 0 OR channel2_nudge_claimed_at <= ?)",
1456+
"UPDATE session_meta SET channel2_nudge_state = 'pending', channel2_nudge_claimed_at = 0, channel2_nudge_claim_token = '' WHERE channel2_nudge_state = 'claimed' AND (channel2_nudge_claimed_at IS NULL OR channel2_nudge_claimed_at = 0 OR channel2_nudge_claimed_at <= ?)",
14551457
).run(staleBefore);
14561458
} catch {
14571459
// Columns may be missing on a very fresh DB before ensureColumn/migration

packages/plugin/src/features/magic-context/storage-meta-persisted.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,9 @@ export function resetLastNudgeCycleIfTailShrank(
964964
// 'claimed' — a delivery attempt is in flight (CAS-claimed before send);
965965
// `channel2_nudge_claimed_at` stores the lease timestamp so boot
966966
// recovery only rewinds stale claims, never a live sibling send.
967+
// OpenCode also writes `channel2_nudge_claim_token` so a slow
968+
// sender cannot confirm a lease after another process heals and
969+
// re-delivers it.
967970
// 'delivered'— confirmed sent; the one ceiling nudge is consumed (terminal)
968971
// On send failure the caller reverts 'claimed' -> 'pending' so a transient error
969972
// does not permanently burn the single ceiling nudge. After send succeeds, a
@@ -975,7 +978,9 @@ interface PersistedChannel2StateRow {
975978
}
976979

977980
interface PersistedChannel2ClaimRow {
981+
channel2_nudge_state?: string;
978982
channel2_nudge_claimed_at: number;
983+
channel2_nudge_claim_token?: string | null;
979984
}
980985

981986
function isChannel2StateRow(row: unknown): row is PersistedChannel2StateRow {
@@ -1006,6 +1011,37 @@ export function getChannel2NudgeClaimedAt(db: Database, sessionId: string): numb
10061011
: 0;
10071012
}
10081013

1014+
export interface Channel2NudgeClaim {
1015+
state: Channel2NudgeState;
1016+
claimedAt: number;
1017+
claimToken: string;
1018+
}
1019+
1020+
export function getChannel2NudgeClaim(db: Database, sessionId: string): Channel2NudgeClaim {
1021+
const result = db
1022+
.prepare(
1023+
"SELECT channel2_nudge_state, channel2_nudge_claimed_at, channel2_nudge_claim_token FROM session_meta WHERE session_id = ?",
1024+
)
1025+
.get(sessionId) as PersistedChannel2ClaimRow | null;
1026+
const rawState =
1027+
typeof result?.channel2_nudge_state === "string" ? result.channel2_nudge_state : "";
1028+
const state: Channel2NudgeState =
1029+
rawState === "pending" || rawState === "claimed" || rawState === "delivered"
1030+
? rawState
1031+
: "";
1032+
return {
1033+
state,
1034+
claimedAt:
1035+
typeof result?.channel2_nudge_claimed_at === "number"
1036+
? result.channel2_nudge_claimed_at
1037+
: 0,
1038+
claimToken:
1039+
typeof result?.channel2_nudge_claim_token === "string"
1040+
? result.channel2_nudge_claim_token
1041+
: "",
1042+
};
1043+
}
1044+
10091045
export function setChannel2NudgeState(
10101046
db: Database,
10111047
sessionId: string,
@@ -1015,7 +1051,7 @@ export function setChannel2NudgeState(
10151051
ensureSessionMetaRow(db, sessionId);
10161052
const claimedAt = state === "claimed" ? Date.now() : 0;
10171053
db.prepare(
1018-
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ? WHERE session_id = ?",
1054+
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = '' WHERE session_id = ?",
10191055
).run(state, claimedAt, sessionId);
10201056
})();
10211057
}
@@ -1037,14 +1073,53 @@ export function casChannel2NudgeState(
10371073
const claimedAt = to === "claimed" ? Date.now() : 0;
10381074
const result = db
10391075
.prepare(
1040-
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ? WHERE session_id = ? AND channel2_nudge_state = ?",
1076+
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = '' WHERE session_id = ? AND channel2_nudge_state = ?",
10411077
)
10421078
.run(to, claimedAt, sessionId, from);
10431079
changed = (result.changes ?? 0) > 0;
10441080
})();
10451081
return changed;
10461082
}
10471083

1084+
export function claimChannel2NudgeState(
1085+
db: Database,
1086+
sessionId: string,
1087+
claimToken: string,
1088+
): boolean {
1089+
let changed = false;
1090+
db.transaction(() => {
1091+
ensureSessionMetaRow(db, sessionId);
1092+
const result = db
1093+
.prepare(
1094+
"UPDATE session_meta SET channel2_nudge_state = 'claimed', channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = ? WHERE session_id = ? AND channel2_nudge_state = 'pending'",
1095+
)
1096+
.run(Date.now(), claimToken, sessionId);
1097+
changed = (result.changes ?? 0) > 0;
1098+
})();
1099+
return changed;
1100+
}
1101+
1102+
export function casChannel2NudgeClaim(
1103+
db: Database,
1104+
sessionId: string,
1105+
to: Channel2NudgeState,
1106+
claimToken: string,
1107+
): boolean {
1108+
let changed = false;
1109+
db.transaction(() => {
1110+
ensureSessionMetaRow(db, sessionId);
1111+
const claimedAt = to === "claimed" ? Date.now() : 0;
1112+
const nextClaimToken = to === "claimed" ? claimToken : "";
1113+
const result = db
1114+
.prepare(
1115+
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = ? WHERE session_id = ? AND channel2_nudge_state = 'claimed' AND channel2_nudge_claim_token = ?",
1116+
)
1117+
.run(to, claimedAt, nextClaimToken, sessionId, claimToken);
1118+
changed = (result.changes ?? 0) > 0;
1119+
})();
1120+
return changed;
1121+
}
1122+
10481123
export function getPersistedNoteNudge(db: Database, sessionId: string): PersistedNoteNudge {
10491124
const result = db
10501125
.prepare(

packages/plugin/src/features/magic-context/storage-meta.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export {
99
appendNoteNudgeAnchor,
1010
applyStrippedPlaceholderDelta,
1111
type Channel2NudgeState,
12+
casChannel2NudgeClaim,
1213
casChannel2NudgeState,
14+
claimChannel2NudgeState,
1315
clearDeferredExecutePendingIfMatches,
1416
clearDetectedContextLimit,
1517
clearEmergencyDrainLatch,
@@ -25,6 +27,7 @@ export {
2527
DRAIN_WINDOW_MS,
2628
deliverNoteNudgeAtomic,
2729
getAutoSearchHintDecisions,
30+
getChannel2NudgeClaim,
2831
getChannel2NudgeClaimedAt,
2932
getChannel2NudgeState,
3033
getHistorianFailureState,

packages/plugin/src/features/magic-context/storage-schema-helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ function healNullTextColumns(db: Database): void {
8080
["cache_ttl", ""],
8181
["last_nudge_band", ""],
8282
["last_nudge_level", ""],
83+
["channel2_nudge_claim_token", ""],
8384
["last_transform_error", ""],
8485
["nudge_anchor_message_id", ""],
8586
["nudge_anchor_text", ""],

packages/plugin/src/features/magic-context/storage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ export {
8989
appendNoteNudgeAnchor,
9090
applyStrippedPlaceholderDelta,
9191
type Channel2NudgeState,
92+
casChannel2NudgeClaim,
9293
casChannel2NudgeState,
94+
claimChannel2NudgeState,
9395
clearCachedM0,
9496
clearCachedM0M1,
9597
clearDeferredExecutePendingIfMatches,
@@ -108,6 +110,7 @@ export {
108110
DRAIN_WINDOW_MS,
109111
deliverNoteNudgeAtomic,
110112
getAutoSearchHintDecisions,
113+
getChannel2NudgeClaim,
111114
getChannel2NudgeClaimedAt,
112115
getChannel2NudgeState,
113116
getHistorianFailureState,

packages/plugin/src/hooks/magic-context/channel2-delivery.test.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,10 @@ describe("maybeDeliverChannel2", () => {
173173

174174
expect(promptAsync).toHaveBeenCalledTimes(1);
175175
expect(delivered).toBe(false);
176-
expect(getChannel2NudgeState(db, "ses-confirm-lost")).toBe("delivered");
176+
expect(getChannel2NudgeState(db, "ses-confirm-lost")).toBe("");
177177
});
178178

179-
it("preserves a sibling's delivered claim and logs the duplicate window distinctly", async () => {
179+
it("preserves a sibling's delivered claim when token confirmation is no longer ours", async () => {
180180
useTempDataHome("ch2-duplicate-window-");
181181
const db = openDatabase()!;
182182
const sessionId = "ses-duplicate-window";
@@ -206,11 +206,46 @@ describe("maybeDeliverChannel2", () => {
206206
(call) =>
207207
call[0] === sessionId &&
208208
typeof call[1] === "string" &&
209-
call[1].includes("duplicate window"),
209+
call[1].includes("confirmation was not ours"),
210210
),
211211
).toBe(true);
212212
});
213213

214+
it("does not stale-confirm when a healed mid-send claim is re-delivered elsewhere", async () => {
215+
useTempDataHome("ch2-healed-mid-send-");
216+
const db = openDatabase()!;
217+
const sessionId = "ses-healed-mid-send";
218+
setChannel2NudgeState(db, sessionId, "pending");
219+
220+
const secondPromptAsync = mock(async () => ({}));
221+
const firstPromptAsync = mock(async () => {
222+
// Simulate boot healing a stale claim while the original promptAsync is
223+
// still in flight, then a sibling process delivering the rewound intent.
224+
db.prepare(
225+
"UPDATE session_meta SET channel2_nudge_state = 'pending', channel2_nudge_claimed_at = 0, channel2_nudge_claim_token = '' WHERE session_id = ?",
226+
).run(sessionId);
227+
const secondDelivered = await maybeDeliverChannel2(sessionId, {
228+
db,
229+
client: fakeClient(secondPromptAsync),
230+
reclaimableTokens: 30_000,
231+
usableTokens: 60_000,
232+
});
233+
expect(secondDelivered).toBe(true);
234+
});
235+
236+
const delivered = await maybeDeliverChannel2(sessionId, {
237+
db,
238+
client: fakeClient(firstPromptAsync),
239+
reclaimableTokens: 30_000,
240+
usableTokens: 60_000,
241+
});
242+
243+
expect(firstPromptAsync).toHaveBeenCalledTimes(1);
244+
expect(secondPromptAsync).toHaveBeenCalledTimes(1);
245+
expect(delivered).toBe(false);
246+
expect(getChannel2NudgeState(db, sessionId)).toBe("delivered");
247+
});
248+
214249
it("leaves a stale claim healable when claimed→pending CAS throws on send failure", async () => {
215250
useTempDataHome("ch2-revert-throw-");
216251
const db = openDatabase()!;
@@ -222,20 +257,22 @@ describe("maybeDeliverChannel2", () => {
222257
const statement = originalPrepare(sql);
223258
if (
224259
sql ===
225-
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ? WHERE session_id = ? AND channel2_nudge_state = ?"
260+
"UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = ? WHERE session_id = ? AND channel2_nudge_state = 'claimed' AND channel2_nudge_claim_token = ?"
226261
) {
227262
return {
228263
...statement,
229264
run: (...args: unknown[]) => {
230265
if (
231266
args[0] === "pending" &&
232267
args[1] === 0 &&
233-
args[2] === sessionId &&
234-
args[3] === "claimed"
268+
args[2] === "" &&
269+
args[3] === sessionId
235270
) {
236271
throw new Error("SQLITE_BUSY: database is locked");
237272
}
238-
return statement.run(...(args as [unknown, unknown, unknown, unknown]));
273+
return statement.run(
274+
...(args as [unknown, unknown, unknown, unknown, unknown]),
275+
);
239276
},
240277
} as typeof statement;
241278
}

0 commit comments

Comments
 (0)