context-mode 1.0.104 → 1.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -148,6 +148,29 @@ export declare class SessionDB extends SQLiteBase {
148
148
  * Mark the resume snapshot as consumed (already injected into conversation).
149
149
  */
150
150
  markResumeConsumed(sessionId: string): void;
151
+ /**
152
+ * Atomically claim the most recent unconsumed resume snapshot in this DB,
153
+ * EXCLUDING any row that belongs to `currentSessionId`.
154
+ *
155
+ * `SessionDB` is sharded per project (see `getSessionDBPath` — SHA-256 of
156
+ * project dir), so "this DB" already implies "this project". The atomic
157
+ * `UPDATE … RETURNING` ensures concurrent processes for the same project
158
+ * cannot both inject the same snapshot (Mickey / PR #376 race).
159
+ *
160
+ * The `currentSessionId` parameter prevents self-injection: when a session
161
+ * compacts mid-flight and produces its own row, that session's next chat
162
+ * turn must NOT claim that row back (wasted tokens AND it would consume
163
+ * the snapshot meant for the next fresh session).
164
+ *
165
+ * Pass an empty string to allow self-claim (legacy behaviour, only useful
166
+ * in tests or one-off harnesses).
167
+ *
168
+ * Returns null when no unconsumed snapshot exists for any other session.
169
+ */
170
+ claimLatestUnconsumedResume(currentSessionId: string): {
171
+ sessionId: string;
172
+ snapshot: string;
173
+ } | null;
151
174
  /**
152
175
  * Return the most recent session_id from session_meta, or null if none.
153
176
  * Used by the runtime to attach persistent counters to the right session
@@ -87,6 +87,7 @@ const S = {
87
87
  upsertResume: "upsertResume",
88
88
  getResume: "getResume",
89
89
  markResumeConsumed: "markResumeConsumed",
90
+ claimLatestUnconsumedResume: "claimLatestUnconsumedResume",
90
91
  deleteEvents: "deleteEvents",
91
92
  deleteMeta: "deleteMeta",
92
93
  deleteResume: "deleteResume",
@@ -251,6 +252,25 @@ export class SessionDB extends SQLiteBase {
251
252
  consumed = 0`);
252
253
  p(S.getResume, `SELECT snapshot, event_count, consumed FROM session_resume WHERE session_id = ?`);
253
254
  p(S.markResumeConsumed, `UPDATE session_resume SET consumed = 1 WHERE session_id = ?`);
255
+ // Atomic "pick newest unconsumed snapshot AND mark it consumed in one
256
+ // statement". Required for race-safe cross-session resume injection
257
+ // (Mickey / PR #376) — two parallel chat-turn hooks must not both read
258
+ // the same row before either one writes consumed=1.
259
+ //
260
+ // The `session_id != ?` clause prevents self-injection (v1.0.106): when
261
+ // Session B compacts mid-flight and produces its own row, B's next chat
262
+ // turn must NOT claim that row back into its own prompt — that's wasted
263
+ // tokens and steals the snapshot meant for the next fresh session.
264
+ p(S.claimLatestUnconsumedResume, `UPDATE session_resume
265
+ SET consumed = 1
266
+ WHERE id = (
267
+ SELECT id FROM session_resume
268
+ WHERE consumed = 0
269
+ AND session_id != ?
270
+ ORDER BY created_at DESC, id DESC
271
+ LIMIT 1
272
+ )
273
+ RETURNING session_id, snapshot`);
254
274
  // ── Delete ──
255
275
  p(S.deleteEvents, `DELETE FROM session_events WHERE session_id = ?`);
256
276
  p(S.deleteMeta, `DELETE FROM session_meta WHERE session_id = ?`);
@@ -478,6 +498,31 @@ export class SessionDB extends SQLiteBase {
478
498
  markResumeConsumed(sessionId) {
479
499
  this.stmt(S.markResumeConsumed).run(sessionId);
480
500
  }
501
+ /**
502
+ * Atomically claim the most recent unconsumed resume snapshot in this DB,
503
+ * EXCLUDING any row that belongs to `currentSessionId`.
504
+ *
505
+ * `SessionDB` is sharded per project (see `getSessionDBPath` — SHA-256 of
506
+ * project dir), so "this DB" already implies "this project". The atomic
507
+ * `UPDATE … RETURNING` ensures concurrent processes for the same project
508
+ * cannot both inject the same snapshot (Mickey / PR #376 race).
509
+ *
510
+ * The `currentSessionId` parameter prevents self-injection: when a session
511
+ * compacts mid-flight and produces its own row, that session's next chat
512
+ * turn must NOT claim that row back (wasted tokens AND it would consume
513
+ * the snapshot meant for the next fresh session).
514
+ *
515
+ * Pass an empty string to allow self-claim (legacy behaviour, only useful
516
+ * in tests or one-off harnesses).
517
+ *
518
+ * Returns null when no unconsumed snapshot exists for any other session.
519
+ */
520
+ claimLatestUnconsumedResume(currentSessionId) {
521
+ const row = this.stmt(S.claimLatestUnconsumedResume).get(currentSessionId);
522
+ if (!row)
523
+ return null;
524
+ return { sessionId: row.session_id, snapshot: row.snapshot };
525
+ }
481
526
  /**
482
527
  * Return the most recent session_id from session_meta, or null if none.
483
528
  * Used by the runtime to attach persistent counters to the right session