@cotal-ai/connector-core 0.69.0 → 0.70.0
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.
- package/dist/agent.d.ts +9 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +18 -24
- package/dist/agent.js.map +1 -1
- package/dist/agui-holder.d.ts +57 -27
- package/dist/agui-holder.d.ts.map +1 -1
- package/dist/agui-holder.js +29 -36
- package/dist/agui-holder.js.map +1 -1
- package/dist/docs-bundle.generated.d.ts +1 -1
- package/dist/docs-bundle.generated.js +21 -21
- package/dist/docs-bundle.generated.js.map +1 -1
- package/dist/event-wal.d.ts +38 -44
- package/dist/event-wal.d.ts.map +1 -1
- package/dist/event-wal.js +105 -124
- package/dist/event-wal.js.map +1 -1
- package/dist/framing.d.ts +1 -18
- package/dist/framing.d.ts.map +1 -1
- package/dist/framing.js +20 -3
- package/dist/framing.js.map +1 -1
- package/dist/orientation.d.ts +2 -2
- package/dist/orientation.d.ts.map +1 -1
- package/dist/orientation.js +17 -4
- package/dist/orientation.js.map +1 -1
- package/dist/tool-specs.js +4 -4
- package/dist/tool-specs.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs-bundle.generated.js","sourceRoot":"","sources":["../src/docs-bundle.generated.ts"],"names":[],"mappings":"AAKA,yFAAyF;AACzF,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC;AAErC,MAAM,UAAU,cAAc;IAC5B,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,eAAe,EAAE,mEAAmE;QACpF,OAAO,EAAE;YACP;gBACE,MAAM,EAAE,eAAe;gBACvB,OAAO,EAAE,eAAe;gBACxB,MAAM,EAAE,0BAA0B;gBAClC,SAAS,EAAE,+FAA+F;gBAC1G,MAAM,EAAE,81IAA81I;aACv2I;YACD;gBACE,MAAM,EAAE,iBAAiB;gBACzB,OAAO,EAAE,YAAY;gBACrB,MAAM,EAAE,0BAA0B;gBAClC,SAAS,EAAE,gHAAgH;gBAC3H,MAAM,EAAE,g2XAAg2X;aACz2X;YACD;gBACE,MAAM,EAAE,cAAc;gBACtB,OAAO,EAAE,cAAc;gBACvB,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,wMAAwM;gBACnN,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"docs-bundle.generated.js","sourceRoot":"","sources":["../src/docs-bundle.generated.ts"],"names":[],"mappings":"AAKA,yFAAyF;AACzF,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC;AAErC,MAAM,UAAU,cAAc;IAC5B,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,eAAe,EAAE,mEAAmE;QACpF,OAAO,EAAE;YACP;gBACE,MAAM,EAAE,eAAe;gBACvB,OAAO,EAAE,eAAe;gBACxB,MAAM,EAAE,0BAA0B;gBAClC,SAAS,EAAE,+FAA+F;gBAC1G,MAAM,EAAE,81IAA81I;aACv2I;YACD;gBACE,MAAM,EAAE,iBAAiB;gBACzB,OAAO,EAAE,YAAY;gBACrB,MAAM,EAAE,0BAA0B;gBAClC,SAAS,EAAE,gHAAgH;gBAC3H,MAAM,EAAE,g2XAAg2X;aACz2X;YACD;gBACE,MAAM,EAAE,cAAc;gBACtB,OAAO,EAAE,cAAc;gBACvB,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,wMAAwM;gBACnN,MAAM,EAAE,upnBAAupnB;aAChqnB;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,kBAAkB;gBAC3B,MAAM,EAAE,mEAAmE;gBAC3E,SAAS,EAAE,wMAAwM;gBACnN,MAAM,EAAE,ugqCAAugqC;aAChhqC;YACD;gBACE,MAAM,EAAE,0BAA0B;gBAClC,OAAO,EAAE,qBAAqB;gBAC9B,MAAM,EAAE,mCAAmC;gBAC3C,SAAS,EAAE,gGAAgG;gBAC3G,MAAM,EAAE,6qMAA6qM;aACtrM;YACD;gBACE,MAAM,EAAE,mBAAmB;gBAC3B,OAAO,EAAE,UAAU;gBACnB,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,oDAAoD;gBAC/D,MAAM,EAAE,wr1CAAwr1C;aACjs1C;YACD;gBACE,MAAM,EAAE,aAAa;gBACrB,OAAO,EAAE,aAAa;gBACtB,MAAM,EAAE,yFAAyF;gBACjG,SAAS,EAAE,gJAAgJ;gBAC3J,MAAM,EAAE,i3VAAi3V;aAC13V;YACD;gBACE,MAAM,EAAE,uBAAuB;gBAC/B,OAAO,EAAE,uBAAuB;gBAChC,MAAM,EAAE,sFAAsF;gBAC9F,SAAS,EAAE,6GAA6G;gBACxH,MAAM,EAAE,y/SAAy/S;aAClgT;YACD;gBACE,MAAM,EAAE,gBAAgB;gBACxB,OAAO,EAAE,sBAAsB;gBAC/B,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,sMAAsM;gBACjN,MAAM,EAAE,4+SAA4+S;aACr/S;YACD;gBACE,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,uBAAuB;gBAChC,MAAM,EAAE,wGAAwG;gBAChH,SAAS,EAAE,iKAAiK;gBAC5K,MAAM,EAAE,ot8KAAot8K;aAC7t8K;YACD;gBACE,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,eAAe;gBACxB,MAAM,EAAE,uHAAuH;gBAC/H,SAAS,EAAE,wMAAwM;gBACnN,MAAM,EAAE,ku+BAAku+B;aAC3u+B;YACD;gBACE,MAAM,EAAE,gBAAgB;gBACxB,OAAO,EAAE,gBAAgB;gBACzB,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,+EAA+E;gBAC1F,MAAM,EAAE,4syCAA4syC;aACrtyC;YACD;gBACE,MAAM,EAAE,eAAe;gBACvB,OAAO,EAAE,sBAAsB;gBAC/B,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,wMAAwM;gBACnN,MAAM,EAAE,iisBAAiisB;aAC1isB;YACD;gBACE,MAAM,EAAE,gBAAgB;gBACxB,OAAO,EAAE,wBAAwB;gBACjC,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,kJAAkJ;gBAC7J,MAAM,EAAE,4ifAA4if;aACrjf;YACD;gBACE,MAAM,EAAE,eAAe;gBACvB,OAAO,EAAE,sBAAsB;gBAC/B,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,6CAA6C;gBACxD,MAAM,EAAE,4z2BAA4z2B;aACr02B;YACD;gBACE,MAAM,EAAE,kBAAkB;gBAC1B,OAAO,EAAE,yBAAyB;gBAClC,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,wJAAwJ;gBACnK,MAAM,EAAE,+3XAA+3X;aACx4X;YACD;gBACE,MAAM,EAAE,YAAY;gBACpB,OAAO,EAAE,oBAAoB;gBAC7B,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,8DAA8D;gBACzE,MAAM,EAAE,g3QAAg3Q;aACz3Q;YACD;gBACE,MAAM,EAAE,YAAY;gBACpB,OAAO,EAAE,YAAY;gBACrB,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,4HAA4H;gBACvI,MAAM,EAAE,6wMAA6wM;aACtxM;YACD;gBACE,MAAM,EAAE,iBAAiB;gBACzB,OAAO,EAAE,qBAAqB;gBAC9B,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,oIAAoI;gBAC/I,MAAM,EAAE,oviCAAoviC;aAC7viC;YACD;gBACE,MAAM,EAAE,eAAe;gBACvB,OAAO,EAAE,eAAe;gBACxB,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,uMAAuM;gBAClN,MAAM,EAAE,qrSAAqrS;aAC9rS;YACD;gBACE,MAAM,EAAE,iBAAiB;gBACzB,OAAO,EAAE,+BAA+B;gBACxC,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,6HAA6H;gBACxI,MAAM,EAAE,irfAAirf;aAC1rf;YACD;gBACE,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,uBAAuB;gBAChC,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,6GAA6G;gBACxH,MAAM,EAAE,q6MAAq6M;aAC96M;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,iBAAiB;gBAC1B,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,yEAAyE;gBACpF,MAAM,EAAE,mzgEAAmzgE;aAC5zgE;YACD;gBACE,MAAM,EAAE,UAAU;gBAClB,OAAO,EAAE,UAAU;gBACnB,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,6DAA6D;gBACxE,MAAM,EAAE,+6GAA+6G;aACx7G;YACD;gBACE,MAAM,EAAE,UAAU;gBAClB,OAAO,EAAE,UAAU;gBACnB,MAAM,EAAE,yBAAyB;gBACjC,SAAS,EAAE,wEAAwE;gBACnF,MAAM,EAAE,q5PAAq5P;aAC95P;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,qBAAqB;gBAC9B,MAAM,EAAE,yBAAyB;gBACjC,SAAS,EAAE,+CAA+C;gBAC1D,MAAM,EAAE,i0NAAi0N;aAC10N;YACD;gBACE,MAAM,EAAE,UAAU;gBAClB,OAAO,EAAE,8BAA8B;gBACvC,MAAM,EAAE,8CAA8C;gBACtD,SAAS,EAAE,qIAAqI;gBAChJ,MAAM,EAAE,o6SAAo6S;aAC76S;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,UAAU;gBACnB,MAAM,EAAE,sDAAsD;gBAC9D,SAAS,EAAE,uJAAuJ;gBAClK,MAAM,EAAE,srZAAsrZ;aAC/rZ;YACD;gBACE,MAAM,EAAE,uBAAuB;gBAC/B,OAAO,EAAE,cAAc;gBACvB,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,0IAA0I;gBACrJ,MAAM,EAAE,0yhBAA0yhB;aACnzhB;YACD;gBACE,MAAM,EAAE,SAAS;gBACjB,OAAO,EAAE,sBAAsB;gBAC/B,MAAM,EAAE,0CAA0C;gBAClD,SAAS,EAAE,gIAAgI;gBAC3I,MAAM,EAAE,i0aAAi0a;aAC10a;YACD;gBACE,MAAM,EAAE,SAAS;gBACjB,OAAO,EAAE,SAAS;gBAClB,MAAM,EAAE,yBAAyB;gBACjC,SAAS,EAAE,mLAAmL;gBAC9L,MAAM,EAAE,y7KAAy7K;aACl8K;YACD;gBACE,MAAM,EAAE,YAAY;gBACpB,OAAO,EAAE,YAAY;gBACrB,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,wMAAwM;gBACnN,MAAM,EAAE,m19CAAm19C;aAC519C;YACD;gBACE,MAAM,EAAE,UAAU;gBAClB,OAAO,EAAE,gBAAgB;gBACzB,MAAM,EAAE,oCAAoC;gBAC5C,SAAS,EAAE,kGAAkG;gBAC7G,MAAM,EAAE,6xbAA6xb;aACtyb;YACD;gBACE,MAAM,EAAE,iBAAiB;gBACzB,OAAO,EAAE,oCAAoC;gBAC7C,MAAM,EAAE,0CAA0C;gBAClD,SAAS,EAAE,wMAAwM;gBACnN,MAAM,EAAE,o9pBAAo9pB;aAC79pB;YACD;gBACE,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,QAAQ;gBACjB,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,2DAA2D;gBACtE,MAAM,EAAE,swKAAswK;aAC/wK;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,qBAAqB;gBAC9B,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,kGAAkG;gBAC7G,MAAM,EAAE,8uPAA8uP;aACvvP;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,uBAAuB;gBAChC,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,kGAAkG;gBAC7G,MAAM,EAAE,09OAA09O;aACn+O;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,gCAAgC;gBACzC,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,iEAAiE;gBAC5E,MAAM,EAAE,444DAA444D;aACr54D;YACD;gBACE,MAAM,EAAE,cAAc;gBACtB,OAAO,EAAE,cAAc;gBACvB,MAAM,EAAE,qBAAqB;gBAC7B,SAAS,EAAE,uHAAuH;gBAClI,MAAM,EAAE,s+qBAAs+qB;aAC/+qB;YACD;gBACE,MAAM,EAAE,WAAW;gBACnB,OAAO,EAAE,eAAe;gBACxB,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,kHAAkH;gBAC7H,MAAM,EAAE,ys1CAAys1C;aAClt1C;SACF;QACD,MAAM,EAAE;YACN,OAAO,EAAE,0BAA0B;YACnC,MAAM,EAAE,s7igBAAs7igB;SAC/7igB;QACD,MAAM,EAAE;YACN,OAAO,EAAE,mCAAmC;YAC5C,MAAM,EAAE,qziGAAqziG;SAC9ziG;QACD,QAAQ,EAAE;YACR,OAAO,EAAE,oCAAoC;YAC7C,MAAM,EAAE,m4pBAAm4pB;SAC54pB;KACF,CAAC;AACF,CAAC"}
|
package/dist/event-wal.d.ts
CHANGED
|
@@ -42,10 +42,10 @@ export interface WalPending {
|
|
|
42
42
|
/**
|
|
43
43
|
* The frame's parts, FROZEN at transition 1 beside the id and `E` that identify them.
|
|
44
44
|
*
|
|
45
|
-
* Without this the WAL
|
|
46
|
-
* `sent_unacked`
|
|
45
|
+
* Without this the WAL would freeze what NAMES a frame and not the frame: a restart holding
|
|
46
|
+
* `sent_unacked` would recover the id and the expectation and have nothing to re-publish, so
|
|
47
47
|
* "retry the same frame after a crash" — the one thing this file exists to make possible —
|
|
48
|
-
* could not be performed from the document. The write-ahead rule requires it
|
|
48
|
+
* could not be performed from the document. The write-ahead rule requires it.
|
|
49
49
|
*
|
|
50
50
|
* It is the parts and not a rendered message because `multicastExpecting` builds the envelope
|
|
51
51
|
* (`ts`, `from`, `space`) at publish time; storing that too would freeze a second copy of fields
|
|
@@ -76,10 +76,9 @@ export interface WalDoc {
|
|
|
76
76
|
gen: number;
|
|
77
77
|
/**
|
|
78
78
|
* The space this WAL belongs to — stored because it is a PATH COMPONENT and a path component is
|
|
79
|
-
* not a trusted input. `principal` and `threadId`
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* thirds of a three-part guard is not the guard.
|
|
79
|
+
* not a trusted input. Verifying only `principal` and `threadId` on load would let a WAL copied
|
|
80
|
+
* or mis-resolved between two space directories under the same principal and thread LOAD, and
|
|
81
|
+
* adopt one space's frontier as another's. Two thirds of a three-part guard is not the guard.
|
|
83
82
|
*/
|
|
84
83
|
space: string;
|
|
85
84
|
epoch: string;
|
|
@@ -123,9 +122,8 @@ export declare class WalStaleWriterError extends Error {
|
|
|
123
122
|
*
|
|
124
123
|
* **Exported so a test can drive the shipped flags rather than recompose them.** A cell that builds
|
|
125
124
|
* `O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW` itself is testing a COPY of this rule: it stays green
|
|
126
|
-
* while the production open drifts or loses a flag
|
|
127
|
-
*
|
|
128
|
-
* suite fully green. Driving this function is what makes the mutation land.
|
|
125
|
+
* while the production open drifts or loses a flag, so a mutation reverting the real open to `"w"`
|
|
126
|
+
* leaves it green. Driving this function is what makes the mutation land.
|
|
129
127
|
*
|
|
130
128
|
* `O_EXCL` refuses a pre-existing file rather than adopting it — which matters because `"w"` does
|
|
131
129
|
* NOT re-chmod an existing inode, so an adopted 0644 temp would carry its mode onto the renamed WAL
|
|
@@ -140,16 +138,14 @@ export declare class EventWal {
|
|
|
140
138
|
*
|
|
141
139
|
* Without it, two concurrent `beginSend` calls both read `this.doc.pending === null` before either
|
|
142
140
|
* durable replace finishes — the guard is an in-memory read that is NOT atomic with the write
|
|
143
|
-
* across its `await` points.
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
141
|
+
* across its `await` points. The split leaves one call fulfilled, one rejected, and the process
|
|
142
|
+
* holding `pending.id === "A"` in memory while the disk says `"B"`. Recovery would then resume
|
|
143
|
+
* the frame on disk while the live emitter retries the other, which breaks the one thing this
|
|
144
|
+
* file exists to guarantee — that `id` and `E` are frozen and agreed.
|
|
147
145
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* chains and both of them "succeed". That gap was described here as "solved upstream by the
|
|
152
|
-
* principal-level lock" while no lock was ever acquired. Two things close it now, and neither is
|
|
146
|
+
* The chain serializes THIS INSTANCE's callers and nothing more. It does nothing about a SECOND
|
|
147
|
+
* `EventWal` on the same file, in this process or another — the chain is per object, so two
|
|
148
|
+
* objects are two chains and both of them "succeed". Two things close that gap, and neither is
|
|
153
149
|
* this chain: `acquirePrincipalLock` refuses a second emitter for the principal at start, and
|
|
154
150
|
* {@link EventWal.assertNotClobbering} refuses a stale handle's write even when it got past that.
|
|
155
151
|
*/
|
|
@@ -189,11 +185,11 @@ export declare class EventWal {
|
|
|
189
185
|
/**
|
|
190
186
|
* Bind the PRINCIPAL-scoped subject frontier this thread publishes onto.
|
|
191
187
|
*
|
|
192
|
-
* **THE TIP IS NOT THIS THREAD'S TO REMEMBER
|
|
188
|
+
* **THE TIP IS NOT THIS THREAD'S TO REMEMBER.**
|
|
193
189
|
* `frontier.lastSubjectSeq` records the last sequence THIS thread was assigned, which is a true
|
|
194
|
-
* fact about this log and
|
|
195
|
-
*
|
|
196
|
-
* had filled, and
|
|
190
|
+
* fact about this log and not the subject's tip. The subject is per principal, so a second
|
|
191
|
+
* session of the same agent reading its own virgin log would expect an empty subject its own
|
|
192
|
+
* predecessor had filled, and halt forever. Once bound, the bound record is authoritative for the
|
|
197
193
|
* expectation and this document's own number is history.
|
|
198
194
|
*
|
|
199
195
|
* Called once, by {@link AguiEmitter.start}, which is the only thing that drives a WAL toward a
|
|
@@ -205,14 +201,12 @@ export declare class EventWal {
|
|
|
205
201
|
/**
|
|
206
202
|
* The sequence a publish must expect, which is the SUBJECT's tip and not this thread's.
|
|
207
203
|
*
|
|
208
|
-
* **UNBOUND IT THROWS
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* prose. So the assumption is a guard now. A caller that drives a log toward a publish without a
|
|
215
|
-
* frontier fails here rather than republishing an expectation that was never the subject's.
|
|
204
|
+
* **UNBOUND IT THROWS**, and it never falls back to this document's own last ack: that number is
|
|
205
|
+
* per session, while the subject is per principal. No shipped path reaches the unbound case,
|
|
206
|
+
* because {@link AguiEmitter.start} is the only route from a log to a publish and it binds before
|
|
207
|
+
* the emitter exists, and the throw makes that a guard rather than an assumption recorded in
|
|
208
|
+
* prose. A caller that drives a log toward a publish without a frontier fails here rather than
|
|
209
|
+
* republishing an expectation that was never the subject's.
|
|
216
210
|
*/
|
|
217
211
|
get expectedTip(): number;
|
|
218
212
|
/** Transition 1 — record the frame, with `id` and `E` frozen, BEFORE any publish. */
|
|
@@ -261,24 +255,24 @@ export declare class EventWal {
|
|
|
261
255
|
* (that message has landed); the only window where it is dangerous is exactly the window this
|
|
262
256
|
* file holds it in, between transition 1 and the ack.
|
|
263
257
|
*
|
|
264
|
-
* So the
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
258
|
+
* So the exposure is gated on a local read OF THIS FILE rather than on mesh access. A
|
|
259
|
+
* world-readable WAL would convert a property the design credits to entropy into one credited to
|
|
260
|
+
* filesystem luck. Under our own rules the attack yields a LOUD halt rather than silent loss — a
|
|
261
|
+
* duplicate ack on a retry fails loud with the frontier and cursor unmoved — so this is denial of
|
|
262
|
+
* service, not corruption. Closing it by construction is cheap enough that naming it as an
|
|
263
|
+
* accepted residual would be the worse trade.
|
|
270
264
|
*/
|
|
271
265
|
private write;
|
|
272
266
|
/**
|
|
273
267
|
* Refuse to replace a document this handle did not read.
|
|
274
268
|
*
|
|
275
|
-
* **THE FAILURE THIS EXISTS FOR
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
269
|
+
* **THE FAILURE THIS EXISTS FOR.** Two `EventWal` objects are open on one file. A runs the full
|
|
270
|
+
* cycle and folds a frontier of `{seq:1, lastSubjectSeq:5}`. B, whose in-memory document is
|
|
271
|
+
* frozen back at the pending write, then calls `recordAck(99)` and `fold()` — without this check
|
|
272
|
+
* both SUCCEED, each replacing the whole file, and the WAL comes back up claiming a durable tip
|
|
273
|
+
* of 99: a subject sequence the broker never assigned. The next publish freezes `E := 99`
|
|
274
|
+
* against a stream whose real tip is 5, so the emitter either CAS-halts forever or recovers a
|
|
275
|
+
* frontier that never existed. Nothing about that is loud; it reads as a healthy WAL.
|
|
282
276
|
*
|
|
283
277
|
* The per-instance `serialize` chain cannot see it (two instances, two chains) and neither can the
|
|
284
278
|
* principal lock (B's handle predates any lock B would take, and a lock is not held against a
|
package/dist/event-wal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-wal.d.ts","sourceRoot":"","sources":["../src/event-wal.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnF,OAAO,EAAiB,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,MAAM,WAAW,WAAW;IAC1B,4FAA4F;IAC5F,GAAG,EAAE,MAAM,CAAC;IACZ,qFAAqF;IACrF,cAAc,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC;IAChC,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAC;IACX,kFAAkF;IAClF,CAAC,EAAE,MAAM,CAAC;IACV,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ
|
|
1
|
+
{"version":3,"file":"event-wal.d.ts","sourceRoot":"","sources":["../src/event-wal.ts"],"names":[],"mappings":"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"}
|
package/dist/event-wal.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* THE ORDERING RULE THE WHOLE DESIGN RESTS ON. Transition 1 records `sent_unacked` and only THEN
|
|
10
10
|
* publishes; transition 2 records `acked` on a NON-duplicate ack, durably, BEFORE the frontier
|
|
11
11
|
* moves; transition 3 folds the frontier and clears pending. The `acked` state looks redundant
|
|
12
|
-
* until you remove it:
|
|
12
|
+
* until you remove it: committing the cursor and then unlinking pending looks safe on the
|
|
13
13
|
* reasoning that a crash between them would "re-publish, and CAS or in-window dedupe absorbs it".
|
|
14
14
|
* That is false under these same rules. Pending still holds the PRE-STORE `E`, the tip has moved to
|
|
15
15
|
* `E+1`, so recovery takes a CAS loss — which is fail-loud — and the emitter wedges FOREVER on a
|
|
@@ -100,9 +100,8 @@ function docGeneration(path, doc) {
|
|
|
100
100
|
*
|
|
101
101
|
* **Exported so a test can drive the shipped flags rather than recompose them.** A cell that builds
|
|
102
102
|
* `O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW` itself is testing a COPY of this rule: it stays green
|
|
103
|
-
* while the production open drifts or loses a flag
|
|
104
|
-
*
|
|
105
|
-
* suite fully green. Driving this function is what makes the mutation land.
|
|
103
|
+
* while the production open drifts or loses a flag, so a mutation reverting the real open to `"w"`
|
|
104
|
+
* leaves it green. Driving this function is what makes the mutation land.
|
|
106
105
|
*
|
|
107
106
|
* `O_EXCL` refuses a pre-existing file rather than adopting it — which matters because `"w"` does
|
|
108
107
|
* NOT re-chmod an existing inode, so an adopted 0644 temp would carry its mode onto the renamed WAL
|
|
@@ -125,23 +124,23 @@ export async function openExclusiveNoFollow(path) {
|
|
|
125
124
|
* Named relations rather than one compound boolean: an operator who cannot see WHICH relation broke
|
|
126
125
|
* cannot act on it, and a single fused check gives a mutation only one cell to kill.
|
|
127
126
|
*
|
|
128
|
-
* **THERE IS DELIBERATELY NO `sourceCursor` RELATION HERE
|
|
129
|
-
* An earlier version asserted `pending.sourceCursor >= frontier.sourceCursor` using string `<`.
|
|
127
|
+
* **THERE IS DELIBERATELY NO `sourceCursor` RELATION HERE.**
|
|
130
128
|
* A cursor is **OPAQUE** — `DurableSource` defines it per source and states that a caller "never
|
|
131
129
|
* parses it". `JsonlFileSource` happens to emit `dev:ino:offset:seal`, and lexicographic order is
|
|
132
|
-
* NOT offset order once the digit width changes, so
|
|
130
|
+
* NOT offset order once the digit width changes, so `pending.sourceCursor >= frontier.sourceCursor`
|
|
131
|
+
* by string `<` is wrong in BOTH directions:
|
|
133
132
|
*
|
|
134
|
-
* frontier offset 8, acked pending offset 45 →
|
|
135
|
-
* frontier offset 170, acked pending offset 98 →
|
|
133
|
+
* frontier offset 8, acked pending offset 45 → REFUSES a healthy WAL ("45" < "8")
|
|
134
|
+
* frontier offset 170, acked pending offset 98 → ACCEPTS a mixed-vintage one ("98" > "170")
|
|
136
135
|
*
|
|
137
136
|
* The first wedges recovery on ordinary source growth across any digit boundary — on the exact
|
|
138
137
|
* crash window the `acked` state exists to survive. The second silently admits the mixed vintage
|
|
139
|
-
*
|
|
138
|
+
* it is meant to refuse.
|
|
140
139
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
140
|
+
* Changing the operator does not help: **ordering an opaque value requires parsing it**, and
|
|
141
|
+
* parsing it would bind this WAL to one source's format — breaking the abstraction the moment a
|
|
142
|
+
* store-backed source uses something else (OpenCode's `part.id`, a rollout path, an API token).
|
|
143
|
+
* The relation cannot be stated correctly at this layer, so it is not stated at all.
|
|
145
144
|
*
|
|
146
145
|
* What remains is sufficient and well-defined, because it uses the WAL's OWN monotonic counters
|
|
147
146
|
* rather than a foreign key: an acked frame must be exactly the frontier's successor, and its
|
|
@@ -151,15 +150,11 @@ export async function openExclusiveNoFollow(path) {
|
|
|
151
150
|
function assertPendingVintage(path, p, f) {
|
|
152
151
|
// ── RELATIONS THAT HOLD FOR *EITHER* TAG ──
|
|
153
152
|
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// The asymmetry was mine: I wrote three named relations for the path that has already succeeded
|
|
161
|
-
// and almost none for the path that is still in flight. Found by fmae-rev-sec, reproduced by
|
|
162
|
-
// fmae-rev-eng and fmae-rev-wal, then here.
|
|
153
|
+
// `sent_unacked` is the crash window transition 1 exists to survive, so it is held to these as
|
|
154
|
+
// strictly as `acked`. A document with `E` disagreeing with the frontier's tip, or a `seq` that is
|
|
155
|
+
// not the frontier's successor, would otherwise LOAD and then retry a frozen expectation that
|
|
156
|
+
// cannot be the honest successor of this frontier: a permanent CAS halt, or a publish at the
|
|
157
|
+
// wrong stream position, with no loud corrupt at open.
|
|
163
158
|
if (p.E !== f.lastSubjectSeq)
|
|
164
159
|
throw new WalCorruptError(path, "pending.E === frontier.lastSubjectSeq", `pending.E=${p.E} frontier.lastSubjectSeq=${f.lastSubjectSeq} — the frozen expectation is not ` +
|
|
165
160
|
`the tip this WAL believes, so retrying it can only CAS-halt or append at the wrong position`);
|
|
@@ -179,7 +174,7 @@ function assertPendingVintage(path, p, f) {
|
|
|
179
174
|
`vintage than the acked frame, so its sourceCursor may already have passed events that were ` +
|
|
180
175
|
`never published; resuming would drop them with no gap for a consumer to see`);
|
|
181
176
|
// NO sourceCursor comparison — see the note above. The cursor is opaque; ordering it here is not
|
|
182
|
-
// a thing this layer can do correctly
|
|
177
|
+
// a thing this layer can do correctly.
|
|
183
178
|
}
|
|
184
179
|
/**
|
|
185
180
|
* Validate a persisted bracket state, or refuse it.
|
|
@@ -258,10 +253,10 @@ function parseDoc(path, raw, space, threadId, principal) {
|
|
|
258
253
|
throw new WalCorruptError(path, "frontier.sourceCursor is a string or absent", typeof f.sourceCursor);
|
|
259
254
|
// A NONZERO FRONTIER MUST CARRY THE POSITION IT WAS DERIVED FROM.
|
|
260
255
|
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
256
|
+
// No shipped transition produces a nonzero frontier without a cursor, and that is the reason to
|
|
257
|
+
// check for one rather than a reason to skip it. A recovery component must refuse the states its
|
|
258
|
+
// own writer CANNOT produce, because those are precisely the states corruption produces. The
|
|
259
|
+
// happy path is not the input domain.
|
|
265
260
|
//
|
|
266
261
|
// The cost of accepting it is silent loss, and it is not theoretical: the emitter resumes by
|
|
267
262
|
// reading forward from this cursor, and `read(undefined)` does not resume — it ADOPTS AT THE
|
|
@@ -284,12 +279,10 @@ function parseDoc(path, raw, space, threadId, principal) {
|
|
|
284
279
|
// reclassifies "we may have published and do not know" as "we have nothing in flight", which is
|
|
285
280
|
// the one downgrade a write-ahead log must never make on its own authority.
|
|
286
281
|
//
|
|
287
|
-
// ONE check, not two.
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// asserting that outcome proves neither of them — so the belt came off and the cells now bite on
|
|
292
|
-
// the one guard that does the work.
|
|
282
|
+
// ONE check, not two. An absent key is `undefined`, and the shape check refuses it with the same
|
|
283
|
+
// invariant, so a separate `hasOwnProperty` guard would be a second mechanism preventing one
|
|
284
|
+
// outcome, and a cell asserting that outcome would prove neither of them. The cells bite on the
|
|
285
|
+
// one guard that does the work.
|
|
293
286
|
if (doc.pending !== null && typeof doc.pending !== "object")
|
|
294
287
|
throw new WalCorruptError(path, "pending is present (null or an object)", doc.pending === undefined ? "the key is absent, which is not the same as null" : typeof doc.pending);
|
|
295
288
|
let pending = null;
|
|
@@ -300,11 +293,11 @@ function parseDoc(path, raw, space, threadId, principal) {
|
|
|
300
293
|
if (typeof p.id !== "string" || p.id.length === 0)
|
|
301
294
|
throw new WalCorruptError(path, "pending.id is a non-empty string", String(p.id));
|
|
302
295
|
// ...and it must satisfy the WIRE grammar, not merely be a string. `beginSend` validates with
|
|
303
|
-
// `assertIdToken` on the way IN
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
296
|
+
// `assertIdToken` on the way IN, and open checks the same on the way OUT: a corrupted or
|
|
297
|
+
// hand-edited id (`"has.dots"`) ADOPTED at recovery would be rejected by `multicastExpecting` on
|
|
298
|
+
// every republish attempt. That turns disk corruption into a permanent runtime wedge instead of
|
|
299
|
+
// a refusal at open, which inverts this file's posture. Validated with the SHIPPED grammar
|
|
300
|
+
// rather than a second copy that could drift from it.
|
|
308
301
|
try {
|
|
309
302
|
assertIdToken(p.id, "event WAL pending.id");
|
|
310
303
|
}
|
|
@@ -364,16 +357,14 @@ export class EventWal {
|
|
|
364
357
|
*
|
|
365
358
|
* Without it, two concurrent `beginSend` calls both read `this.doc.pending === null` before either
|
|
366
359
|
* durable replace finishes — the guard is an in-memory read that is NOT atomic with the write
|
|
367
|
-
* across its `await` points.
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
360
|
+
* across its `await` points. The split leaves one call fulfilled, one rejected, and the process
|
|
361
|
+
* holding `pending.id === "A"` in memory while the disk says `"B"`. Recovery would then resume
|
|
362
|
+
* the frame on disk while the live emitter retries the other, which breaks the one thing this
|
|
363
|
+
* file exists to guarantee — that `id` and `E` are frozen and agreed.
|
|
371
364
|
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* chains and both of them "succeed". That gap was described here as "solved upstream by the
|
|
376
|
-
* principal-level lock" while no lock was ever acquired. Two things close it now, and neither is
|
|
365
|
+
* The chain serializes THIS INSTANCE's callers and nothing more. It does nothing about a SECOND
|
|
366
|
+
* `EventWal` on the same file, in this process or another — the chain is per object, so two
|
|
367
|
+
* objects are two chains and both of them "succeed". Two things close that gap, and neither is
|
|
377
368
|
* this chain: `acquirePrincipalLock` refuses a second emitter for the principal at start, and
|
|
378
369
|
* {@link EventWal.assertNotClobbering} refuses a stale handle's write even when it got past that.
|
|
379
370
|
*/
|
|
@@ -424,11 +415,11 @@ export class EventWal {
|
|
|
424
415
|
if (bytes !== undefined) {
|
|
425
416
|
// FATAL UTF-8 — never `readFile(path, "utf8")`. Node's default decoder SUBSTITUTES U+FFFD for
|
|
426
417
|
// invalid bytes, so a corrupted file arrives as a changed-but-parseable document: a single raw
|
|
427
|
-
// 0xff inside the epoch string
|
|
418
|
+
// 0xff inside the epoch string loads cleanly with `epoch` silently rewritten. For a file whose
|
|
428
419
|
// whole posture is that every unreadable state fails loud, "quietly altered and accepted" is
|
|
429
420
|
// the one outcome it must not produce — the identity bytes recovery depends on would be the
|
|
430
|
-
// decoder's invention. `JsonlFileSource` in this same package
|
|
431
|
-
//
|
|
421
|
+
// decoder's invention. `JsonlFileSource` in this same package decodes fatally for exactly
|
|
422
|
+
// this class, and this is the same rule.
|
|
432
423
|
try {
|
|
433
424
|
raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
434
425
|
}
|
|
@@ -470,11 +461,11 @@ export class EventWal {
|
|
|
470
461
|
/**
|
|
471
462
|
* Bind the PRINCIPAL-scoped subject frontier this thread publishes onto.
|
|
472
463
|
*
|
|
473
|
-
* **THE TIP IS NOT THIS THREAD'S TO REMEMBER
|
|
464
|
+
* **THE TIP IS NOT THIS THREAD'S TO REMEMBER.**
|
|
474
465
|
* `frontier.lastSubjectSeq` records the last sequence THIS thread was assigned, which is a true
|
|
475
|
-
* fact about this log and
|
|
476
|
-
*
|
|
477
|
-
* had filled, and
|
|
466
|
+
* fact about this log and not the subject's tip. The subject is per principal, so a second
|
|
467
|
+
* session of the same agent reading its own virgin log would expect an empty subject its own
|
|
468
|
+
* predecessor had filled, and halt forever. Once bound, the bound record is authoritative for the
|
|
478
469
|
* expectation and this document's own number is history.
|
|
479
470
|
*
|
|
480
471
|
* Called once, by {@link AguiEmitter.start}, which is the only thing that drives a WAL toward a
|
|
@@ -493,23 +484,20 @@ export class EventWal {
|
|
|
493
484
|
if (this.subject)
|
|
494
485
|
throw new Error(`event WAL ${this.path}: a DIFFERENT subject frontier is already bound`);
|
|
495
486
|
this.subject = frontier;
|
|
496
|
-
// NO SEEDING HERE
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
// own `open`.
|
|
487
|
+
// NO SEEDING HERE. The upgrade case is a NEW thread whose own log is virgin while the sequence
|
|
488
|
+
// sits in a PREVIOUS thread's log, so seeding from `this` document recovers nothing in exactly
|
|
489
|
+
// the case that matters. That recovery belongs to whoever can see every thread log of the
|
|
490
|
+
// principal, which is the record's own `open`.
|
|
501
491
|
}
|
|
502
492
|
/**
|
|
503
493
|
* The sequence a publish must expect, which is the SUBJECT's tip and not this thread's.
|
|
504
494
|
*
|
|
505
|
-
* **UNBOUND IT THROWS
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* prose. So the assumption is a guard now. A caller that drives a log toward a publish without a
|
|
512
|
-
* frontier fails here rather than republishing an expectation that was never the subject's.
|
|
495
|
+
* **UNBOUND IT THROWS**, and it never falls back to this document's own last ack: that number is
|
|
496
|
+
* per session, while the subject is per principal. No shipped path reaches the unbound case,
|
|
497
|
+
* because {@link AguiEmitter.start} is the only route from a log to a publish and it binds before
|
|
498
|
+
* the emitter exists, and the throw makes that a guard rather than an assumption recorded in
|
|
499
|
+
* prose. A caller that drives a log toward a publish without a frontier fails here rather than
|
|
500
|
+
* republishing an expectation that was never the subject's.
|
|
513
501
|
*/
|
|
514
502
|
get expectedTip() {
|
|
515
503
|
if (!this.subject)
|
|
@@ -523,11 +511,10 @@ export class EventWal {
|
|
|
523
511
|
return this.serialize(async () => {
|
|
524
512
|
if (this.doc.pending)
|
|
525
513
|
throw new Error(`event WAL ${this.path}: a frame is already pending; one emit unit is one pending frame`);
|
|
526
|
-
// THE SAME GRAMMAR THE WIRE USES, not a restatement of it.
|
|
527
|
-
// (" ", a newline, 65 chars, dots)
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
// that true rather than asserted.
|
|
514
|
+
// THE SAME GRAMMAR THE WIRE USES, not a restatement of it. An id that `multicastExpecting`'s
|
|
515
|
+
// `assertIdToken` rejects (" ", a newline, 65 chars, dots) would let T1 freeze an id on disk
|
|
516
|
+
// that can NEVER publish, wedging the emitter until an abandon. The id is one token on the wire
|
|
517
|
+
// and in the WAL; importing the check is what makes that true rather than asserted.
|
|
531
518
|
assertIdToken(frame.id, "event WAL pending id");
|
|
532
519
|
if (frame.E !== this.expectedTip)
|
|
533
520
|
throw new Error(`event WAL ${this.path}: E=${frame.E} is not the subject's tip ${this.expectedTip}`);
|
|
@@ -550,26 +537,24 @@ export class EventWal {
|
|
|
550
537
|
const p = this.doc.pending;
|
|
551
538
|
if (!p || p.state !== "sent_unacked")
|
|
552
539
|
throw new Error(`event WAL ${this.path}: no sent_unacked frame to ack`);
|
|
553
|
-
// VALIDATE BEFORE THE DURABLE WRITE. `recordAck(-1)`
|
|
554
|
-
// `frontier.lastSubjectSeq = -1`, and the NEXT open
|
|
555
|
-
//
|
|
556
|
-
// not on the boot after it.
|
|
540
|
+
// VALIDATE BEFORE THE DURABLE WRITE. An accepted `recordAck(-1)` would let `fold()` persist
|
|
541
|
+
// `frontier.lastSubjectSeq = -1`, and the NEXT open would refuse the file — a single bad ack
|
|
542
|
+
// durably bricking the WAL while every call reported success. Fail-closed has to happen before
|
|
543
|
+
// the write, not on the boot after it.
|
|
557
544
|
if (!isSafeNonNegInt(ackSeq))
|
|
558
545
|
throw new Error(`event WAL ${this.path}: ackSeq must be a safe non-negative integer, got ${String(ackSeq)}`);
|
|
559
546
|
if (ackSeq <= this.expectedTip)
|
|
560
547
|
throw new Error(`event WAL ${this.path}: ackSeq=${ackSeq} is not ahead of the subject's tip ${this.expectedTip}`);
|
|
561
548
|
// TWO RULES ABOUT THE NEXT TWO LINES, AND THEY ARE KEPT TOGETHER SO NO PROSE SITS BETWEEN THEM.
|
|
562
549
|
//
|
|
563
|
-
// A STALE WRITER MAY NOT TOUCH THE SHARED RECORD AT ALL,
|
|
564
|
-
// inside `write
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
// that arrives after the mutation refuses the wrong thing.
|
|
550
|
+
// A STALE WRITER MAY NOT TOUCH THE SHARED RECORD AT ALL, so the refusal runs here and not only
|
|
551
|
+
// inside `write`, which is AFTER the record moves: there, a handle whose file had been rewritten
|
|
552
|
+
// underneath it would advance the principal's tip and only then learn it was not allowed to
|
|
553
|
+
// write. The record is shared by every thread of the principal; a refusal that arrives after
|
|
554
|
+
// the mutation refuses the wrong thing.
|
|
569
555
|
//
|
|
570
|
-
// THEN THE SHARED RECORD ADVANCES FIRST
|
|
571
|
-
//
|
|
572
|
-
// is the record AHEAD of this log: the frame landed, the record took the sequence the broker
|
|
556
|
+
// THEN THE SHARED RECORD ADVANCES FIRST. What a crash between these two lines leaves is the
|
|
557
|
+
// record AHEAD of this log: the frame landed, the record took the sequence the broker
|
|
573
558
|
// assigned it, and the log still holds that frame as `sent_unacked` with its `E` frozen at the
|
|
574
559
|
// tip BEFORE it. The next start republishes the frozen expectation, the subject has already
|
|
575
560
|
// passed it, and a single-replica stream evaluates the expectation BEFORE the dedup cache, so
|
|
@@ -586,14 +571,13 @@ export class EventWal {
|
|
|
586
571
|
// of this principal reads its expectation from. Both orders fail closed. Only one of them fails
|
|
587
572
|
// closed on a value the whole principal reads.
|
|
588
573
|
//
|
|
589
|
-
// A LOCAL, NOT `this.subject?.advance(ackSeq)`.
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
// as a mutant that could only ever survive.
|
|
574
|
+
// A LOCAL, NOT `this.subject?.advance(ackSeq)`. `expectedTip` above throws when nothing is
|
|
575
|
+
// bound, so an unbound log never reaches this statement, but an optional call would be the
|
|
576
|
+
// wrong SHAPE all the same: it leans on that line staying above this one, and a comment
|
|
577
|
+
// claiming the line above throws cannot survive that line being moved or deleted; a local the
|
|
578
|
+
// compiler checks can. The refusal below is therefore unreachable by construction and NO CELL
|
|
579
|
+
// GRADES IT, which is said here rather than registered as a mutant that could only ever
|
|
580
|
+
// survive.
|
|
597
581
|
const subject = this.subject;
|
|
598
582
|
if (!subject)
|
|
599
583
|
throw new Error(`event WAL ${this.path}: no subject frontier is bound, so this ack has no shared record to advance`);
|
|
@@ -651,11 +635,10 @@ export class EventWal {
|
|
|
651
635
|
// goes with it. Leaving it standing would make the next thread expect a tip the subject no
|
|
652
636
|
// longer has, which is this defect's mirror image: the same permanent halt from the other side.
|
|
653
637
|
//
|
|
654
|
-
// REQUIRED, NEVER `this.subject?.reset()`.
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
// as whole.
|
|
638
|
+
// REQUIRED, NEVER `this.subject?.reset()`. With no record bound, an optional call would clear
|
|
639
|
+
// the log's half and report a completed abandonment, leaving the shared half standing. This
|
|
640
|
+
// method's own contract above is that partial abandonment is not a state, and an optional call
|
|
641
|
+
// is how a partial one gets reported as whole.
|
|
659
642
|
await this.assertNotClobbering();
|
|
660
643
|
if (!this.subject)
|
|
661
644
|
throw new Error(`event WAL ${this.path}: no subject frontier is bound, so an abandonment here would clear ` +
|
|
@@ -687,33 +670,31 @@ export class EventWal {
|
|
|
687
670
|
* (that message has landed); the only window where it is dangerous is exactly the window this
|
|
688
671
|
* file holds it in, between transition 1 and the ack.
|
|
689
672
|
*
|
|
690
|
-
* So the
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
673
|
+
* So the exposure is gated on a local read OF THIS FILE rather than on mesh access. A
|
|
674
|
+
* world-readable WAL would convert a property the design credits to entropy into one credited to
|
|
675
|
+
* filesystem luck. Under our own rules the attack yields a LOUD halt rather than silent loss — a
|
|
676
|
+
* duplicate ack on a retry fails loud with the frontier and cursor unmoved — so this is denial of
|
|
677
|
+
* service, not corruption. Closing it by construction is cheap enough that naming it as an
|
|
678
|
+
* accepted residual would be the worse trade.
|
|
696
679
|
*/
|
|
697
680
|
async write(next) {
|
|
698
681
|
// NOBODY ELSE HAS WRITTEN THIS FILE SINCE THIS HANDLE READ IT. Checked FIRST, because every
|
|
699
682
|
// line below replaces the document wholesale.
|
|
700
683
|
await this.assertNotClobbering();
|
|
701
684
|
const stamped = { ...next, gen: this.doc.gen + 1 };
|
|
702
|
-
// The temp name is RANDOM per write, and the open is EXCLUSIVE and NON-FOLLOWING
|
|
703
|
-
//
|
|
704
|
-
// The previous version derived the name from `path` + `pid` — predictable — and used
|
|
705
|
-
// `open(tmp, "w", 0o600)`. Both halves were exploitable and both were reproduced:
|
|
685
|
+
// The temp name is RANDOM per write, and the open is EXCLUSIVE and NON-FOLLOWING, because a
|
|
686
|
+
// predictable name (`path` + `pid`) opened with `open(tmp, "w", 0o600)` is exploitable twice:
|
|
706
687
|
// - `open(…, "w")` does not re-chmod an EXISTING inode; the mode argument applies only on
|
|
707
|
-
// create. A planted 0644 temp
|
|
708
|
-
// `pending.id` — a pre-publication secret by this class's own argument —
|
|
709
|
-
// world-readable
|
|
710
|
-
// - `"w"` follows symlinks. A symlink planted at the predicted name
|
|
711
|
-
// truncate and overwrite an arbitrary file the process could open. One plant,
|
|
712
|
-
//
|
|
688
|
+
// create. A planted 0644 temp would survive as the WAL's mode, and the file holding
|
|
689
|
+
// `pending.id` — a pre-publication secret by this class's own argument — would end up
|
|
690
|
+
// world-readable.
|
|
691
|
+
// - `"w"` follows symlinks. A symlink planted at the predicted name would make the next
|
|
692
|
+
// transition truncate and overwrite an arbitrary file the process could open. One plant,
|
|
693
|
+
// one write.
|
|
713
694
|
//
|
|
714
695
|
// O_EXCL makes a pre-existing temp a hard failure rather than something to adopt; O_NOFOLLOW
|
|
715
|
-
// refuses a symlink outright; the random suffix removes the predictability that
|
|
716
|
-
//
|
|
696
|
+
// refuses a symlink outright; the random suffix removes the predictability that planting
|
|
697
|
+
// relies on; and 0600 comes from the CREATE flags, which is the only place it can come from —
|
|
717
698
|
// `rename` preserves the inode's mode, so there is no post-rename chmod here and this comment
|
|
718
699
|
// does not claim one. The suite asserts the mode on the surviving file, which is where the
|
|
719
700
|
// guarantee has to hold; the code's part is refusing to adopt an existing inode at all.
|
|
@@ -739,13 +720,13 @@ export class EventWal {
|
|
|
739
720
|
/**
|
|
740
721
|
* Refuse to replace a document this handle did not read.
|
|
741
722
|
*
|
|
742
|
-
* **THE FAILURE THIS EXISTS FOR
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
723
|
+
* **THE FAILURE THIS EXISTS FOR.** Two `EventWal` objects are open on one file. A runs the full
|
|
724
|
+
* cycle and folds a frontier of `{seq:1, lastSubjectSeq:5}`. B, whose in-memory document is
|
|
725
|
+
* frozen back at the pending write, then calls `recordAck(99)` and `fold()` — without this check
|
|
726
|
+
* both SUCCEED, each replacing the whole file, and the WAL comes back up claiming a durable tip
|
|
727
|
+
* of 99: a subject sequence the broker never assigned. The next publish freezes `E := 99`
|
|
728
|
+
* against a stream whose real tip is 5, so the emitter either CAS-halts forever or recovers a
|
|
729
|
+
* frontier that never existed. Nothing about that is loud; it reads as a healthy WAL.
|
|
749
730
|
*
|
|
750
731
|
* The per-instance `serialize` chain cannot see it (two instances, two chains) and neither can the
|
|
751
732
|
* principal lock (B's handle predates any lock B would take, and a lock is not held against a
|