@remnic/core 10.0.75 → 10.0.77

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.
Files changed (84) hide show
  1. package/dist/access-admin-ops-surface.js +2 -2
  2. package/dist/access-authorization-probe.js +3 -3
  3. package/dist/access-boundary.js +2 -2
  4. package/dist/access-cli.js +17 -17
  5. package/dist/access-extraction-force-flush.js +2 -2
  6. package/dist/access-http-briefing.js +3 -3
  7. package/dist/access-http-query.js +2 -2
  8. package/dist/access-http.js +14 -14
  9. package/dist/access-identity-continuity-surface.js +2 -2
  10. package/dist/access-lcm-surface.js +2 -2
  11. package/dist/access-mcp.js +9 -9
  12. package/dist/access-namespace-preflight.js +2 -2
  13. package/dist/access-observe-helpers.d.ts.map +1 -1
  14. package/dist/access-observe-helpers.js +1 -1
  15. package/dist/access-observe-write-surface.js +2 -2
  16. package/dist/access-operations-batch.js +3 -3
  17. package/dist/access-operations.js +8 -8
  18. package/dist/access-recall-concurrency.js +2 -2
  19. package/dist/access-recall-response.js +2 -2
  20. package/dist/access-recall-surface.js +2 -2
  21. package/dist/access-service.js +2 -2
  22. package/dist/access-surface-catalog.js +4 -4
  23. package/dist/{chunk-R24RYSQX.js → chunk-2JRLKDHV.js} +2 -2
  24. package/dist/{chunk-N6GEGRCT.js → chunk-4NDDRYBY.js} +2 -2
  25. package/dist/{chunk-VEWTHVX2.js → chunk-5HVEYVNQ.js} +2 -2
  26. package/dist/{chunk-KAED7IBB.js → chunk-6L62IDS6.js} +2 -2
  27. package/dist/{chunk-IPHIIRDN.js → chunk-CYTTY4M2.js} +5 -7
  28. package/dist/chunk-CYTTY4M2.js.map +1 -0
  29. package/dist/{chunk-SZBZOUHR.js → chunk-D26XMQP5.js} +2 -2
  30. package/dist/{chunk-VPEMGBTI.js → chunk-D3USVQIV.js} +2 -2
  31. package/dist/{chunk-E22CQZ3H.js → chunk-EYIBIGIU.js} +8 -8
  32. package/dist/{chunk-IBQ7LGUJ.js → chunk-FQU65RJX.js} +3 -3
  33. package/dist/{chunk-FP2AT4WL.js → chunk-G3PBNYDH.js} +2 -2
  34. package/dist/{chunk-VYOAYFDV.js → chunk-I4W2ADBP.js} +144 -26
  35. package/dist/chunk-I4W2ADBP.js.map +1 -0
  36. package/dist/{chunk-ZG57BEUS.js → chunk-LXDERMOU.js} +2 -2
  37. package/dist/{chunk-SY4AFWUZ.js → chunk-MAURCRPQ.js} +4 -4
  38. package/dist/{chunk-3GGCMRO7.js → chunk-MUZWS6KB.js} +2 -2
  39. package/dist/{chunk-URS56HGW.js → chunk-MXQKGSMW.js} +2 -2
  40. package/dist/{chunk-RXGFITWS.js → chunk-NUBOY2LQ.js} +2 -2
  41. package/dist/{chunk-PLGBPFEO.js → chunk-TWHVCSLX.js} +2 -2
  42. package/dist/{chunk-VEQVXNWR.js → chunk-TX627XOF.js} +2 -2
  43. package/dist/cli.js +15 -15
  44. package/dist/external-wiki-access.js +3 -3
  45. package/dist/index.js +17 -17
  46. package/dist/memory-browse-operations.js +3 -3
  47. package/dist/orchestration/backlog-scheduler.js +18 -18
  48. package/dist/orchestration/buffer-drain-runner.js +18 -18
  49. package/dist/orchestration/extraction-queue-coordinator.d.ts +4 -9
  50. package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
  51. package/dist/orchestration/extraction-queue-snapshot-sharing.d.ts +113 -0
  52. package/dist/orchestration/extraction-queue-snapshot-sharing.d.ts.map +1 -0
  53. package/dist/orchestration/turn-ingestion.d.ts +3 -0
  54. package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
  55. package/dist/orchestrator.js +17 -17
  56. package/dist/recall-navigation-operations.js +3 -3
  57. package/dist/recall-navigation.js +2 -2
  58. package/dist/support-passport/index.js +5 -5
  59. package/package.json +2 -2
  60. package/src/access-observe-helpers.test.ts +156 -0
  61. package/src/access-observe-helpers.ts +6 -6
  62. package/src/orchestration/extraction-queue-bounded-retention.test.ts +548 -0
  63. package/src/orchestration/extraction-queue-coordinator.ts +4 -19
  64. package/src/orchestration/extraction-queue-snapshot-sharing.ts +257 -0
  65. package/src/orchestration/turn-ingestion.ts +19 -6
  66. package/src/orchestrator-extraction-queue.test.ts +70 -13
  67. package/dist/chunk-IPHIIRDN.js.map +0 -1
  68. package/dist/chunk-VYOAYFDV.js.map +0 -1
  69. /package/dist/{chunk-R24RYSQX.js.map → chunk-2JRLKDHV.js.map} +0 -0
  70. /package/dist/{chunk-N6GEGRCT.js.map → chunk-4NDDRYBY.js.map} +0 -0
  71. /package/dist/{chunk-VEWTHVX2.js.map → chunk-5HVEYVNQ.js.map} +0 -0
  72. /package/dist/{chunk-KAED7IBB.js.map → chunk-6L62IDS6.js.map} +0 -0
  73. /package/dist/{chunk-SZBZOUHR.js.map → chunk-D26XMQP5.js.map} +0 -0
  74. /package/dist/{chunk-VPEMGBTI.js.map → chunk-D3USVQIV.js.map} +0 -0
  75. /package/dist/{chunk-E22CQZ3H.js.map → chunk-EYIBIGIU.js.map} +0 -0
  76. /package/dist/{chunk-IBQ7LGUJ.js.map → chunk-FQU65RJX.js.map} +0 -0
  77. /package/dist/{chunk-FP2AT4WL.js.map → chunk-G3PBNYDH.js.map} +0 -0
  78. /package/dist/{chunk-ZG57BEUS.js.map → chunk-LXDERMOU.js.map} +0 -0
  79. /package/dist/{chunk-SY4AFWUZ.js.map → chunk-MAURCRPQ.js.map} +0 -0
  80. /package/dist/{chunk-3GGCMRO7.js.map → chunk-MUZWS6KB.js.map} +0 -0
  81. /package/dist/{chunk-URS56HGW.js.map → chunk-MXQKGSMW.js.map} +0 -0
  82. /package/dist/{chunk-RXGFITWS.js.map → chunk-NUBOY2LQ.js.map} +0 -0
  83. /package/dist/{chunk-PLGBPFEO.js.map → chunk-TWHVCSLX.js.map} +0 -0
  84. /package/dist/{chunk-VEQVXNWR.js.map → chunk-TX627XOF.js.map} +0 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Bounded-queue retention for automatic extraction snapshots (#3324
3
+ * follow-up).
4
+ *
5
+ * The coordinator's queue keeps every accepted task's own snapshot (that is
6
+ * the #3324 contract: per-entry captured scope and settlement), but nothing
7
+ * requires each queued snapshot to RETAIN its own turn objects. Automatic
8
+ * triggers hand out prefix-extended snapshots of the same buffer (the buffer
9
+ * clears only when extraction runs), so consecutive queued snapshots overlap
10
+ * almost entirely while their drain is slow. Retaining one deep copy per
11
+ * snapshot grows quadratic overhead (demonstrated: scaling exponent 1.89 on
12
+ * main, 36× the unique content at 1000 turns for one session).
13
+ *
14
+ * This registry makes the OVERLAP shared without changing ANY task's
15
+ * semantics: a queued snapshot is stored as a chain node
16
+ * `{ base, tail }` whose turns are verified element-wise equivalent to the
17
+ * base chain at admit time. Each task materializes its own fresh array at
18
+ * run time — identical values, same windows, same chunking, same clear
19
+ * matching, same settlements. Sharing is data-structure only.
20
+ *
21
+ * Safety properties:
22
+ * - Equivalence is CONSUMER-equivalence, not `bufferTurnsEqual` alone: the
23
+ * five fields `bufferTurnsEqual` ignores (extractionContextOnly, parts,
24
+ * rawContent, sourceFormat, importProvenance) are compared strictly
25
+ * (objects by reference identity), so an aliased segment is
26
+ * indistinguishable from a private copy to every downstream reader.
27
+ * - Turn objects are immutable by audit: no code path writes a BufferTurn
28
+ * field or a parts array in place (all writes go to freshly constructed
29
+ * objects: `copyBufferTurn`, the replay `pinned` spread). Audited across
30
+ * the extraction call graph.
31
+ * - Distinct scope pins can never share: pins are turn fields covered by
32
+ * `bufferTurnsEqual`, and the scope-class bucket is a lookup heuristic
33
+ * only — a wrong pairing fails the element-wise check and simply does not
34
+ * share.
35
+ * - Replay, bulk import, force flush, fail-closed callers, and claim-backed
36
+ * execution contexts are excluded (they are not automatic snapshots of the
37
+ * live buffer).
38
+ */
39
+
40
+ import type { BufferTurn } from "../types.js";
41
+ import { bufferTurnsEqual, scopeProfilePlanIdentity } from "../buffer-turn-helpers.js";
42
+
43
+ /** Option surface of `queueBufferedExtraction` relevant to eligibility. */
44
+ export interface AutomaticSharingOptions {
45
+ /** Claim-backed backlog tasks own a different buffer view: never share. */
46
+ execution?: unknown;
47
+ /** Automatic trigger paths set this; forced replay/import do not. */
48
+ preemptable?: boolean;
49
+ failOnExtractionFailure?: boolean;
50
+ forceExtractionAttempt?: boolean;
51
+ /** Fail-closed front flush: never share. */
52
+ preemptRunning?: boolean;
53
+ }
54
+
55
+ export function automaticSnapshotSharingEligible(options: AutomaticSharingOptions): boolean {
56
+ return (
57
+ options.execution === undefined &&
58
+ options.preemptable === true &&
59
+ options.failOnExtractionFailure !== true &&
60
+ options.forceExtractionAttempt !== true &&
61
+ options.preemptRunning !== true
62
+ );
63
+ }
64
+
65
+ /** Consumer-visible equivalence: `bufferTurnsEqual` PLUS the fields it ignores. */
66
+ export function turnRepresentationEquivalent(left: BufferTurn, right: BufferTurn): boolean {
67
+ return (
68
+ bufferTurnsEqual(left, right) &&
69
+ left.extractionContextOnly === right.extractionContextOnly &&
70
+ left.importProvenance === right.importProvenance &&
71
+ left.parts === right.parts &&
72
+ left.rawContent === right.rawContent &&
73
+ left.sourceFormat === right.sourceFormat
74
+ );
75
+ }
76
+
77
+ /** Scope-class bucket key, the same composite `groupTurnsByExtractionScope` uses. */
78
+ function scopeClassKey(turn: BufferTurn): string {
79
+ return [
80
+ turn.writeNamespaceOverride ?? "",
81
+ turn.principalOverride ?? "",
82
+ scopeProfilePlanIdentity(turn.scopeProfileWritePlan),
83
+ turn.acceptedCapture === true ? "accepted" : "",
84
+ turn.codingContext?.projectId ?? "",
85
+ turn.codingContext?.rootPath ?? "",
86
+ turn.codingContext?.branch ?? "",
87
+ turn.codingContext?.defaultBranch ?? "",
88
+ ].join("\u0000");
89
+ }
90
+
91
+ /** One queued automatic snapshot: `base` chain plus this trigger's own new turns. */
92
+ export interface SnapshotChain {
93
+ readonly key: string;
94
+ readonly scopeClass: string;
95
+ readonly base: SnapshotChain | undefined;
96
+ /** Total turns retained by the base chain. */
97
+ readonly baseLength: number;
98
+ /** This trigger's non-shared suffix (fresh reference slots, disjoint across the chain). */
99
+ readonly tail: readonly BufferTurn[];
100
+ /** Total turns this snapshot represents. */
101
+ readonly total: number;
102
+ /** Outstanding tasks holding this node; the node leaves the registry when it reaches 0. */
103
+ holders: number;
104
+ }
105
+
106
+ export interface SnapshotSharingStats {
107
+ /** Distinct content chains registered (not task count — see queuedHolders). */
108
+ queuedNodes: number;
109
+ /** Σ per-node window lengths over registered nodes. Sibling nodes sharing
110
+ * a base chain count that base once per node (per-task window basis). */
111
+ queuedTotalRefs: number;
112
+ /** Distinct turn references REACHABLE from queued nodes, counting
113
+ * released ancestors reached through `base` exactly once (visited-set). */
114
+ queuedTailRefs: number;
115
+ /** Longest base chain reachable from a queued node (must stay bounded). */
116
+ queuedReachableDepth: number;
117
+ /** Outstanding tasks holding nodes (per-task, not per-node). */
118
+ queuedHolders: number;
119
+ /** What retention would be WITHOUT sharing: Σ per-node window length ×
120
+ * holders (each outstanding task would carry its own full window). */
121
+ unsharedEquivalentRefs: number;
122
+ }
123
+
124
+ /** Iterative root-first walk: recursive `yield*` delegation costs O(depth)
125
+ * per item and hangs production-scale chains (3,000+ turn sessions,
126
+ * hours-long backlogs). Same order, bounded stack, no casts. */
127
+ function chainNodes(chain: SnapshotChain): SnapshotChain[] {
128
+ const nodes: SnapshotChain[] = [];
129
+ for (let node: SnapshotChain | undefined = chain; node; node = node.base) nodes.push(node);
130
+ return [...nodes].reverse();
131
+ }
132
+
133
+ /**
134
+ * Element-wise consumer-equivalence of the chain's turns against a snapshot
135
+ * prefix. Cost is O(chain.total) compares per admit — required to prove the
136
+ * shared prefix is consumer-equivalent to the incoming snapshot — so a chain
137
+ * build is O(depth^2) compares in total (measured, acknowledged in the PR).
138
+ */
139
+ export function chainPrefixEquivalent(chain: SnapshotChain, turns: readonly BufferTurn[]): boolean {
140
+ let offset = 0;
141
+ for (const node of chainNodes(chain)) {
142
+ for (const chained of node.tail) {
143
+ const incoming = turns[offset];
144
+ if (incoming === undefined) return false;
145
+ if (!turnRepresentationEquivalent(chained, incoming)) return false;
146
+ offset += 1;
147
+ }
148
+ }
149
+ return offset === chain.total;
150
+ }
151
+
152
+ /** Fresh array with exactly this snapshot's turns, in order. Run-time only. */
153
+ export function materializeChain(chain: SnapshotChain): BufferTurn[] {
154
+ const out: BufferTurn[] = [];
155
+ for (const node of chainNodes(chain)) {
156
+ for (const turn of node.tail) out.push(turn);
157
+ }
158
+ return out;
159
+ }
160
+
161
+ export class ExtractionSnapshotSharingRegistry {
162
+ private readonly buckets = new Map<string, SnapshotChain[]>();
163
+
164
+ /**
165
+ * Admit one automatic snapshot for `bufferKey`: chain it onto the newest
166
+ * queued same-scope snapshot when that is a verified prefix-extension,
167
+ * otherwise start a fresh chain. The caller's array is never mutated.
168
+ */
169
+ admit(bufferKey: string, turns: readonly BufferTurn[]): SnapshotChain {
170
+ const scopeClass = turns.length > 0 ? scopeClassKey(turns[0]) : "";
171
+ const bucketKey = `${bufferKey}\u0000${scopeClass}`;
172
+ const list = this.buckets.get(bucketKey);
173
+ const base = list !== undefined && list.length > 0 ? list[list.length - 1] : undefined;
174
+ if (base && base.total === turns.length && chainPrefixEquivalent(base, turns)) {
175
+ // Identical window: reuse the existing node. Creating a tail=[] node
176
+ // here would chain onto the base and grow reachable depth (and admit
177
+ // cost) by one per admission under continuous unchanged ingress, while
178
+ // stats showed only the newest node. Sharing is exact: the element-wise
179
+ // equivalence above proved this window equals the base's. The holder
180
+ // count keeps the node registered until the LAST sharer releases.
181
+ base.holders += 1;
182
+ return base;
183
+ }
184
+ const node: SnapshotChain =
185
+ base && base.total <= turns.length && chainPrefixEquivalent(base, turns)
186
+ ? {
187
+ key: bufferKey,
188
+ scopeClass,
189
+ base,
190
+ baseLength: base.total,
191
+ tail: turns.slice(base.total),
192
+ total: turns.length,
193
+ holders: 1,
194
+ }
195
+ : {
196
+ key: bufferKey,
197
+ scopeClass,
198
+ base: undefined,
199
+ baseLength: 0,
200
+ tail: [...turns],
201
+ total: turns.length,
202
+ holders: 1,
203
+ };
204
+ if (list) list.push(node);
205
+ else this.buckets.set(bucketKey, [node]);
206
+ return node;
207
+ }
208
+
209
+ /** Release one holder of a node when its task exits (or on enqueue
210
+ * refusal). CONTRACT: called EXACTLY ONCE per admit, by the task that
211
+ * admitted it — the registry has no task identity, so a second call by the
212
+ * same task would steal another holder's count. The only production caller
213
+ * (queueBufferedExtraction) enforces this structurally: the closure-finally
214
+ * and the enqueue-refusal branch are mutually exclusive. Defensively, a
215
+ * release past zero is a no-op. */
216
+ release(node: SnapshotChain): void {
217
+ if (node.holders <= 0) return;
218
+ node.holders -= 1;
219
+ if (node.holders > 0) return;
220
+ const list = this.buckets.get(`${node.key}\u0000${node.scopeClass}`);
221
+ if (!list) return;
222
+ const index = list.indexOf(node);
223
+ if (index >= 0) list.splice(index, 1);
224
+ if (list.length === 0) this.buckets.delete(`${node.key}\u0000${node.scopeClass}`);
225
+ }
226
+
227
+ /** Proof seam: what the queue retains right now. */
228
+ stats(): SnapshotSharingStats {
229
+ // Reachability-accurate accounting: a released ancestor can remain
230
+ // reachable through a queued descendant's `base`, so bucket-node sums
231
+ // under-report. Walk each chain once with a visited set of nodes.
232
+ const visited = new Set<SnapshotChain>();
233
+ let queuedNodes = 0;
234
+ let queuedTailRefs = 0;
235
+ let queuedReachableDepth = 0;
236
+ let queuedHolders = 0;
237
+ let unsharedEquivalentRefs = 0;
238
+ let queuedTotalRefs = 0;
239
+ for (const list of this.buckets.values()) {
240
+ for (const node of list) {
241
+ queuedNodes += 1;
242
+ unsharedEquivalentRefs += node.total * node.holders;
243
+ queuedTotalRefs += node.total;
244
+ let depth = 0;
245
+ for (let walk: SnapshotChain | undefined = node; walk; walk = walk.base) {
246
+ depth += 1;
247
+ if (visited.has(walk)) continue;
248
+ visited.add(walk);
249
+ queuedTailRefs += walk.tail.length;
250
+ queuedHolders += walk.holders;
251
+ }
252
+ if (depth > queuedReachableDepth) queuedReachableDepth = depth;
253
+ }
254
+ }
255
+ return { queuedNodes, queuedTotalRefs, queuedTailRefs, queuedReachableDepth, queuedHolders, unsharedEquivalentRefs };
256
+ }
257
+ }
@@ -44,11 +44,8 @@ import type { BufferTurn, PluginConfig, SourceConnectorProvenance } from "../typ
44
44
  import { groupTurnsByExtractionScope, bufferTurnsEqual } from "../buffer-turn-helpers.js";
45
45
  import { ExtractionDrain } from "./extraction-drain.js";
46
46
  import { skipClaimedSession } from "./buffer-drain-gate.js";
47
- import {
48
- queueBufferedCaptureScopeGroups,
49
- type CaptureScopeGroupQueueOptions,
50
- type ExtractionTaskExecutionContext,
51
- } from "./extraction-task-queueing.js";
47
+ import { queueBufferedCaptureScopeGroups, type CaptureScopeGroupQueueOptions, type ExtractionTaskExecutionContext } from "./extraction-task-queueing.js";
48
+ import { automaticSnapshotSharingEligible, ExtractionSnapshotSharingRegistry, materializeChain, type SnapshotChain } from "./extraction-queue-snapshot-sharing.js";
52
49
  import { resolveScopeProfilePlan, type ResolvedScopeProfilePlan, hasExplicitNamespacePolicy } from "../namespaces/scope-profiles.js";
53
50
  import { resolveCodingNamespaceOverlay, combineNamespaces } from "../coding/coding-namespace.js";
54
51
  import { canWriteNamespace, defaultNamespaceForPrincipal } from "../namespaces/principal.js";
@@ -166,6 +163,8 @@ function isFlushPlanRecoveryTurn(
166
163
 
167
164
  export class TurnIngestionCoordinator {
168
165
  private readonly drain: ExtractionDrain; // chunked trigger-drain (extraction-drain.ts)
166
+ /** Queued automatic-snapshot chains: bounded-queue retention via structural sharing. */
167
+ readonly snapshotSharing = new ExtractionSnapshotSharingRegistry();
169
168
  constructor(private readonly deps: TurnIngestionDeps) {
170
169
  this.drain = new ExtractionDrain(deps);
171
170
  }
@@ -902,6 +901,15 @@ export class TurnIngestionCoordinator {
902
901
  }
903
902
  if (settled) return;
904
903
  const preemptable = options.preemptable === true && options.preemptRunning !== true;
904
+ const sharingNode =
905
+ automaticSnapshotSharingEligible(options) && turnsToExtract.length > 0
906
+ ? this.snapshotSharing.admit(bufferKey, turnsToExtract)
907
+ : undefined;
908
+ // Discriminated source so the task closure never captures the caller's
909
+ // snapshot array when sharing: a textual reference would retain every
910
+ // overlapping copy and defeat the bound.
911
+ const queuedTurnsSource: { chain: SnapshotChain } | { direct: BufferTurn[] } =
912
+ sharingNode ? { chain: sharingNode } : { direct: turnsToExtract };
905
913
  const accepted = this.deps.extractionQueueCoordinator.enqueue(async (preemptHandle) => {
906
914
  try {
907
915
  await this.deps.extractionQueueCoordinator.runPreemptable(preemptHandle, preemptable, options.abortSignal, async (abortSignal) => {
@@ -931,7 +939,10 @@ export class TurnIngestionCoordinator {
931
939
  // concurrent drain that already took these turns skips the LLM work,
932
940
  // and re-resolve a pinned plan against CURRENT config — revoke skips
933
941
  // WITHOUT clearing, and the plan is never retargeted.
934
- let effectiveTurns = turnsToExtract;
942
+ let effectiveTurns =
943
+ "chain" in queuedTurnsSource
944
+ ? materializeChain(queuedTurnsSource.chain)
945
+ : queuedTurnsSource.direct;
935
946
  let effectiveScopePlan = options.scopeProfileWritePlan;
936
947
  const executionBuffer = options.execution?.buffer;
937
948
  if (effectiveTurns.some((turn) => turn.acceptedCapture === true)) {
@@ -1083,10 +1094,12 @@ export class TurnIngestionCoordinator {
1083
1094
  }
1084
1095
  });
1085
1096
  } finally {
1097
+ if (sharingNode) this.snapshotSharing.release(sharingNode);
1086
1098
  options.onTaskExit?.();
1087
1099
  }
1088
1100
  }, { front: options.preemptRunning === true, key: bufferKey });
1089
1101
  if (!accepted) {
1102
+ if (sharingNode) this.snapshotSharing.release(sharingNode);
1090
1103
  settleTask(new Error("extraction queue is stopped"));
1091
1104
  options.onTaskExit?.();
1092
1105
  return;
@@ -1,10 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
+ import { queueBufferedCaptureScopeGroups } from "./orchestration/extraction-task-queueing.js";
4
5
  import { ExtractionQueueCoordinator, type ExtractionPreemptHandle, type ExtractionQueueTask } from "./orchestration/extraction-queue-coordinator.js";
5
6
  import { initLogger, type LoggerBackend } from "./logger.js";
6
7
  import { abortError } from "./abort-error.js";
7
8
 
9
+ import type { BufferTurn } from "./types.js";
10
+
8
11
  interface LogEntry {
9
12
  level: "info" | "warn" | "error" | "debug";
10
13
  message: string;
@@ -35,10 +38,9 @@ function createCoordinator(): ExtractionQueueCoordinator {
35
38
  return new ExtractionQueueCoordinator();
36
39
  }
37
40
 
38
- // ── Per-key coalescing: a queued-not-started task is superseded by a newer
39
- // enqueue for the same key instead of stacking another full snapshot ────────
41
+ // ── Per-key FIFO: queued tasks retain their own captured scope and settlement ─
40
42
 
41
- test("a newer enqueue for a queued-not-started key replaces the queued task", async () => {
43
+ test("same-key queued tasks both run in FIFO order", async () => {
42
44
  const coordinator = createCoordinator();
43
45
  const busy = Promise.withResolvers<void>();
44
46
  const releaseBusy = Promise.withResolvers<void>();
@@ -56,15 +58,15 @@ test("a newer enqueue for a queued-not-started key replaces the queued task", as
56
58
  ran.push("fresh");
57
59
  }, { key: "s1" });
58
60
 
59
- assert.equal(coordinator.length, 1, "the stale snapshot must be replaced, not stacked");
61
+ assert.equal(coordinator.length, 2, "same-key tasks remain queued independently");
60
62
 
61
63
  releaseBusy.resolve();
62
64
  assert.equal(await coordinator.waitForIdle(5_000), true);
63
- assert.deepEqual(ran, ["fresh"], "only the newest task for the key runs");
65
+ assert.deepEqual(ran, ["stale", "fresh"], "each caller's task runs in FIFO order");
64
66
  assert.equal(coordinator.isProcessing, false);
65
67
  });
66
68
 
67
- test("coalescing keeps the superseded entry's position for other keys", async () => {
69
+ test("same-key tasks keep their FIFO positions relative to other keys", async () => {
68
70
  const coordinator = createCoordinator();
69
71
  const busy = Promise.withResolvers<void>();
70
72
  const releaseBusy = Promise.withResolvers<void>();
@@ -90,10 +92,10 @@ test("coalescing keeps the superseded entry's position for other keys", async ()
90
92
 
91
93
  releaseBusy.resolve();
92
94
  assert.equal(await coordinator.waitForIdle(5_000), true);
93
- assert.deepEqual(ran, ["k1", "k2-fresh", "k3"], "k2 keeps its queue slot; the newest payload wins");
95
+ assert.deepEqual(ran, ["k1", "k2-stale", "k2-fresh", "k3"]);
94
96
  });
95
97
 
96
- test("a front enqueue supersedes a queued same-key task and keeps preemption", async () => {
98
+ test("a front enqueue preserves queued same-key work and keeps preemption", async () => {
97
99
  const coordinator = createCoordinator();
98
100
  const busy = Promise.withResolvers<void>();
99
101
  const releaseBusy = Promise.withResolvers<void>();
@@ -121,15 +123,15 @@ test("a front enqueue supersedes a queued same-key task and keeps preemption", a
121
123
  ran.push("flush-fresh");
122
124
  }, { key: "s1", front: true });
123
125
 
124
- assert.equal(coordinator.length, 1, "the superseded queued task must be dropped");
126
+ assert.equal(coordinator.length, 2, "front work does not discard queued same-key work");
125
127
 
126
128
  releaseBusy.resolve();
127
129
  assert.equal(await coordinator.waitForIdle(5_000), true);
128
130
  assert.equal(preempted, true, "front enqueue still preempts the running automatic work");
129
- assert.deepEqual(ran, ["flush-fresh"]);
131
+ assert.deepEqual(ran, ["flush-fresh", "queued-stale"]);
130
132
  });
131
133
 
132
- test("keyless tasks never coalesce", async () => {
134
+ test("keyless tasks remain independent", async () => {
133
135
  const coordinator = createCoordinator();
134
136
  const busy = Promise.withResolvers<void>();
135
137
  const releaseBusy = Promise.withResolvers<void>();
@@ -153,7 +155,7 @@ test("keyless tasks never coalesce", async () => {
153
155
  assert.deepEqual(ran, ["first", "second"]);
154
156
  });
155
157
 
156
- test("a coalesced enqueue of a running key waits behind the active task", async () => {
158
+ test("same-key enqueues behind an active task retain both queued tasks in FIFO order", async () => {
157
159
  const coordinator = createCoordinator();
158
160
  const busy = Promise.withResolvers<void>();
159
161
  const releaseBusy = Promise.withResolvers<void>();
@@ -174,7 +176,7 @@ test("a coalesced enqueue of a running key waits behind the active task", async
174
176
 
175
177
  releaseBusy.resolve();
176
178
  assert.equal(await coordinator.waitForIdle(5_000), true);
177
- assert.deepEqual(order, ["running", "queued-second"]);
179
+ assert.deepEqual(order, ["running", "queued-first", "queued-second"]);
178
180
  });
179
181
 
180
182
  // ── Issue #549 ─────────────────────────────────────────────────────────────
@@ -627,3 +629,58 @@ test("tryReserveKey refuses active and queued keys, and releases idempotently",
627
629
  assert.equal(queue.tryReserveKey("s1"), true, "a settled key reserves again");
628
630
  queue.releaseKeyReservation("s1");
629
631
  });
632
+
633
+ test("same-key capture scope groups both run and settle their caller promises", async () => {
634
+ const coordinator = createCoordinator();
635
+ const busy = Promise.withResolvers<void>();
636
+ const releaseBusy = Promise.withResolvers<void>();
637
+ const ran: string[] = [];
638
+ const extractions: Promise<void>[] = [];
639
+
640
+ coordinator.enqueue(async () => {
641
+ busy.resolve();
642
+ await releaseBusy.promise;
643
+ });
644
+ await busy.promise;
645
+
646
+ const acceptedTurns: BufferTurn[] = [
647
+ {
648
+ role: "user",
649
+ content: "scope A",
650
+ timestamp: "2026-11-05T00:00:00.000Z",
651
+ sessionKey: "s1",
652
+ acceptedCapture: true,
653
+ writeNamespaceOverride: "namespace-a",
654
+ principalOverride: "principal-a",
655
+ },
656
+ {
657
+ role: "user",
658
+ content: "scope B",
659
+ timestamp: "2026-11-05T00:00:01.000Z",
660
+ sessionKey: "s1",
661
+ acceptedCapture: true,
662
+ writeNamespaceOverride: "namespace-b",
663
+ principalOverride: "principal-b",
664
+ },
665
+ ];
666
+ queueBufferedCaptureScopeGroups(
667
+ async (_turns, _reason, options) => {
668
+ const accepted = coordinator.enqueue(async () => {
669
+ const namespace = options?.writeNamespaceOverride;
670
+ assert.ok(namespace);
671
+ ran.push(namespace);
672
+ options?.onTaskSettled?.();
673
+ }, { key: options?.bufferKey });
674
+ if (!accepted) throw new Error("queue unexpectedly rejected extraction");
675
+ },
676
+ acceptedTurns,
677
+ "s1",
678
+ { onExtractionQueued: (promise) => extractions.push(promise) },
679
+ );
680
+
681
+ assert.equal(extractions.length, 2, "each captured scope has its own completion promise");
682
+ releaseBusy.resolve();
683
+ assert.equal(await coordinator.waitForIdle(5_000), true);
684
+ await Promise.all(extractions);
685
+ assert.deepEqual(ran, ["namespace-a", "namespace-b"]);
686
+ });
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/access-observe-helpers.ts"],"sourcesContent":["import { isPreemptedExtraction } from \"./abort-error.js\";\n\ntype PendingObservePreparation = {\n cancelled: boolean;\n scopeHint?: string;\n principal?: string;\n namespace?: string;\n scopeResolved: boolean;\n resolvedScopeFences: Array<{ principal?: string; namespace?: string }>;\n released: Promise<void>;\n release: () => void;\n};\n\nfunction resolvedScopesMatch(\n left: { principal?: string; namespace?: string },\n right: { principal?: string; namespace?: string },\n): boolean {\n return left.principal === right.principal && left.namespace === right.namespace;\n}\n\nfunction preparationMatchesScope(\n reservation: PendingObservePreparation,\n principal: string | undefined,\n namespace: string | undefined,\n scopeHint: string | undefined,\n): boolean {\n if (reservation.scopeResolved) {\n return resolvedScopesMatch(reservation, { principal, namespace });\n }\n return reservation.scopeHint === undefined || scopeHint === undefined || reservation.scopeHint === scopeHint;\n}\n\nexport type PendingObservePreparationHandle = {\n isCancelled(): boolean;\n setScope(principal?: string, namespace?: string): void;\n release(): void;\n};\n\nexport function pendingObserveScopeHint(options: {\n namespace?: string;\n projectTag?: string;\n cwd?: string;\n}): string | undefined {\n const namespace = options.namespace?.trim();\n if (namespace) return `namespace:${namespace}`;\n const projectTag = options.projectTag?.trim();\n if (projectTag) return `projectTag:${projectTag}`;\n const cwd = options.cwd?.trim();\n return cwd ? `cwd:${cwd}` : undefined;\n}\n\ntype PendingObserveExtraction = {\n promise: Promise<void>;\n running: Promise<void>;\n controllers: Set<AbortController>;\n};\n\nexport class PendingObserveExtractionTracker {\n private readonly entries = new Map<string, PendingObserveExtraction>();\n private readonly preparations = new Map<string, Set<PendingObservePreparation>>();\n\n reserve(sessionKey: string, scopeHint?: string): PendingObservePreparationHandle {\n let released = false;\n let resolveReleased: () => void = () => {};\n const reservation: PendingObservePreparation = {\n cancelled: false,\n scopeResolved: false,\n resolvedScopeFences: [],\n scopeHint,\n released: new Promise<void>((resolve) => {\n resolveReleased = resolve;\n }),\n release: () => {\n if (released) return;\n released = true;\n const entries = this.preparations.get(sessionKey);\n entries?.delete(reservation);\n if (entries?.size === 0) this.preparations.delete(sessionKey);\n resolveReleased();\n },\n };\n const entries = this.preparations.get(sessionKey) ?? new Set<PendingObservePreparation>();\n entries.add(reservation);\n this.preparations.set(sessionKey, entries);\n return {\n isCancelled: () => reservation.cancelled,\n setScope: (principal, namespace) => {\n reservation.principal = principal;\n reservation.namespace = namespace;\n reservation.scopeResolved = true;\n if (reservation.resolvedScopeFences.some((fence) => resolvedScopesMatch(reservation, fence))) {\n reservation.cancelled = true;\n }\n },\n release: reservation.release,\n };\n }\n\n private async waitForPreparations(\n sessionKey: string,\n principal: string | undefined,\n namespace: string | undefined,\n scopeHint: string | undefined,\n ): Promise<void> {\n while (true) {\n const reservations = [...(this.preparations.get(sessionKey) ?? [])];\n for (const reservation of reservations) {\n if (\n !reservation.cancelled &&\n !reservation.scopeResolved &&\n !preparationMatchesScope(reservation, principal, namespace, scopeHint) &&\n !reservation.resolvedScopeFences.some((fence) =>\n resolvedScopesMatch(fence, { principal, namespace }),\n )\n ) {\n reservation.resolvedScopeFences.push({ principal, namespace });\n }\n }\n const pending = reservations.filter(\n (reservation) =>\n !reservation.cancelled && preparationMatchesScope(reservation, principal, namespace, scopeHint),\n );\n if (pending.length === 0) return;\n await Promise.all(pending.map((reservation) => reservation.released));\n }\n }\n\n\n key(sessionKey: string, principal: string | undefined, namespace: string | undefined): string {\n return `${sessionKey}\\u0000${principal ?? \"\"}\\u0000${namespace ?? \"\"}`;\n }\n\n track(key: string, extraction: Promise<void>, controller: AbortController): void {\n const previous = this.entries.get(key);\n const promise = previous\n ? Promise.allSettled([previous.promise, extraction]).then((results) => {\n const rejected = results.find(\n (result): result is PromiseRejectedResult =>\n result.status === \"rejected\" &&\n !(result.reason instanceof Error && isPreemptedExtraction(result.reason)),\n );\n if (rejected) throw rejected.reason;\n })\n : extraction;\n const entry = {\n promise,\n running: previous?.running ?? extraction,\n controllers: new Set(previous ? [...previous.controllers, controller] : [controller]),\n };\n this.entries.set(key, entry);\n // Any settle (success OR rejection — preemption is just one rejection\n // kind) ends the pending entry; leaving a settled failure tracked wedged\n // the key and kept every controller ever registered for it alive.\n void promise\n .then(() => {\n if (this.entries.get(key) === entry) this.entries.delete(key);\n })\n .catch(() => {\n if (this.entries.get(key) === entry) this.entries.delete(key);\n });\n }\n\n cancelPreparations(sessionKey: string, scopeHint?: string): void {\n for (const reservation of this.preparations.get(sessionKey) ?? []) {\n if (\n scopeHint !== undefined &&\n reservation.scopeHint !== undefined &&\n reservation.scopeHint !== scopeHint\n ) {\n continue;\n }\n reservation.cancelled = true;\n }\n }\n\n cancel(\n sessionKey: string,\n principal?: string,\n namespace?: string,\n scopeHint?: string,\n reason?: Error,\n ): void {\n for (const reservation of this.preparations.get(sessionKey) ?? []) {\n if (!preparationMatchesScope(reservation, principal, namespace, scopeHint)) continue;\n reservation.cancelled = true;\n }\n\n this.abortTracked(sessionKey, principal, namespace, reason);\n }\n\n private abortTracked(\n sessionKey: string,\n principal: string | undefined,\n namespace: string | undefined,\n reason?: Error,\n ): void {\n for (const [key, entry] of this.entries) {\n const [entrySessionKey, entryPrincipal, entryNamespace] = key.split(\"\\u0000\");\n if (\n entrySessionKey !== sessionKey ||\n (principal !== undefined && entryPrincipal !== (principal ?? \"\")) ||\n (namespace !== undefined && entryNamespace !== (namespace ?? \"\"))\n ) continue;\n for (const controller of entry.controllers) controller.abort(reason);\n }\n }\n\n async wait(\n sessionKey: string,\n principal: string | undefined,\n namespace: string | undefined,\n abortSignal?: AbortSignal,\n registerCancellation?: (cancel: () => void) => void,\n scopeHint?: string,\n preemptAfterAdmission?: () => void,\n ): Promise<void> {\n const key = this.key(sessionKey, principal, namespace);\n let cancelled = abortSignal?.aborted === true;\n const abortTracked = (): void => {\n cancelled = true;\n this.cancel(sessionKey, principal, namespace, scopeHint);\n };\n registerCancellation?.(abortTracked);\n abortSignal?.addEventListener(\"abort\", abortTracked, { once: true });\n try {\n await this.waitForPreparations(sessionKey, principal, namespace, scopeHint);\n if (cancelled || abortSignal?.aborted) return;\n if (preemptAfterAdmission) {\n preemptAfterAdmission();\n const running = this.entries.get(key)?.running;\n if (!running) return;\n try {\n await running;\n } catch (error) {\n if (this.entries.get(key)?.running === running) this.entries.delete(key);\n if (!(error instanceof Error && isPreemptedExtraction(error))) throw error;\n }\n return;\n }\n while (true) {\n const entry = this.entries.get(key);\n if (!entry) return;\n try {\n await entry.promise;\n } catch (error) {\n if (this.entries.get(key) !== entry) continue;\n this.entries.delete(key);\n if (error instanceof Error && isPreemptedExtraction(error)) return;\n throw error;\n }\n if (this.entries.get(key) !== entry) continue;\n this.entries.delete(key);\n return;\n }\n } finally {\n abortSignal?.removeEventListener(\"abort\", abortTracked);\n }\n }\n}\n"],"mappings":"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","names":["entries"]}