@ouro.bot/cli 0.1.0-alpha.806 → 0.1.0-alpha.808
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/changelog.json +16 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +156 -188
- package/dist/heart/daemon/cli-exec.js +3 -0
- package/dist/heart/mailbox/readers/sessions.js +2 -3
- package/dist/heart/session-activity.js +3 -1
- package/dist/heart/session-events.js +167 -55
- package/dist/heart/session-redaction-repair-cli-main.js +31 -0
- package/dist/heart/session-redaction-repair.js +543 -0
- package/dist/heart/session-transcript.js +1 -1
- package/dist/mind/session-transaction.js +180 -31
- package/dist/senses/shared-turn.js +5 -3
- package/npm-shrinkwrap.json +94 -100
- package/package.json +2 -2
|
@@ -91,7 +91,90 @@ function isSqliteBusy(error) {
|
|
|
91
91
|
return error instanceof Error && "code" in error
|
|
92
92
|
&& error.code === "SQLITE_BUSY";
|
|
93
93
|
}
|
|
94
|
-
function
|
|
94
|
+
function refuseConfinement() {
|
|
95
|
+
(0, runtime_1.emitNervesEvent)({
|
|
96
|
+
level: "warn", component: "mind", event: "mind.session_confinement_refused",
|
|
97
|
+
message: "session confinement is invalid or changed", meta: { action: "refused" },
|
|
98
|
+
});
|
|
99
|
+
throw new SessionTransactionError("session confinement is invalid or changed");
|
|
100
|
+
}
|
|
101
|
+
function confinedFile(filePath) {
|
|
102
|
+
let stat;
|
|
103
|
+
try {
|
|
104
|
+
stat = fs.lstatSync(filePath);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error?.code === "ENOENT")
|
|
108
|
+
return null;
|
|
109
|
+
return refuseConfinement();
|
|
110
|
+
}
|
|
111
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
112
|
+
return refuseConfinement();
|
|
113
|
+
return stat;
|
|
114
|
+
}
|
|
115
|
+
function checkConfinement(pin) {
|
|
116
|
+
if (!pin)
|
|
117
|
+
return;
|
|
118
|
+
for (const directory of pin.directories) {
|
|
119
|
+
let stat;
|
|
120
|
+
try {
|
|
121
|
+
stat = fs.lstatSync(directory.path);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return refuseConfinement();
|
|
125
|
+
}
|
|
126
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== directory.dev || stat.ino !== directory.ino)
|
|
127
|
+
return refuseConfinement();
|
|
128
|
+
}
|
|
129
|
+
for (const suffix of ["", ".turn.lock", ".turn.lock-journal", ".turn.lock-wal", ".turn.lock-shm"]) {
|
|
130
|
+
confinedFile(`${pin.sessionPath}${suffix}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function pinConfinement(sessionPath, root) {
|
|
134
|
+
if (root === undefined)
|
|
135
|
+
return null;
|
|
136
|
+
if (typeof root !== "string" || !path.isAbsolute(root) || path.resolve(root) !== root)
|
|
137
|
+
return refuseConfinement();
|
|
138
|
+
const relative = path.relative(root, sessionPath);
|
|
139
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
140
|
+
return refuseConfinement();
|
|
141
|
+
const parent = path.dirname(sessionPath);
|
|
142
|
+
const volumeRoot = path.parse(parent).root;
|
|
143
|
+
let current = volumeRoot;
|
|
144
|
+
const paths = [current];
|
|
145
|
+
for (const segment of parent.slice(volumeRoot.length).split(path.sep).filter(Boolean)) {
|
|
146
|
+
current = path.join(current, segment);
|
|
147
|
+
paths.push(current);
|
|
148
|
+
}
|
|
149
|
+
const directories = paths.map((directory) => {
|
|
150
|
+
let stat;
|
|
151
|
+
try {
|
|
152
|
+
stat = fs.lstatSync(directory);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return refuseConfinement();
|
|
156
|
+
}
|
|
157
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
158
|
+
return refuseConfinement();
|
|
159
|
+
return { path: directory, dev: stat.dev, ino: stat.ino };
|
|
160
|
+
});
|
|
161
|
+
const pin = { root, sessionPath, directories };
|
|
162
|
+
checkConfinement(pin);
|
|
163
|
+
return pin;
|
|
164
|
+
}
|
|
165
|
+
function retainConfinement(held, requestedRoot) {
|
|
166
|
+
if (requestedRoot !== undefined && requestedRoot !== held.confinement?.root)
|
|
167
|
+
return refuseConfinement();
|
|
168
|
+
checkConfinement(held.confinement);
|
|
169
|
+
}
|
|
170
|
+
function checkTemporary(pin, filePath, identity) {
|
|
171
|
+
checkConfinement(pin);
|
|
172
|
+
const current = confinedFile(filePath);
|
|
173
|
+
if (!current || !identity || current.dev !== identity.dev || current.ino !== identity.ino)
|
|
174
|
+
return refuseConfinement();
|
|
175
|
+
}
|
|
176
|
+
function openLeaseDatabase(lockPath, busyTimeoutMs = 0, confinement = null) {
|
|
177
|
+
checkConfinement(confinement);
|
|
95
178
|
const database = new better_sqlite3_1.default(lockPath);
|
|
96
179
|
database.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
97
180
|
database.exec(`
|
|
@@ -123,14 +206,15 @@ function acquisitionRecord(options) {
|
|
|
123
206
|
processStartedAt,
|
|
124
207
|
};
|
|
125
208
|
}
|
|
126
|
-
function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt) {
|
|
127
|
-
const database = openLeaseDatabase(lockPath);
|
|
209
|
+
function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt, confinement) {
|
|
210
|
+
const database = openLeaseDatabase(lockPath, 0, confinement);
|
|
128
211
|
try {
|
|
129
212
|
return database.transaction(() => {
|
|
130
213
|
const current = database.prepare(`
|
|
131
214
|
SELECT pid, owner_id, owner_token, boot_identity, process_started_at FROM session_turn_lease WHERE singleton = 1
|
|
132
215
|
`).get();
|
|
133
216
|
if (!current) {
|
|
217
|
+
checkConfinement(confinement);
|
|
134
218
|
database.prepare(`
|
|
135
219
|
INSERT INTO session_turn_lease (singleton, pid, owner_id, owner_token, boot_identity, process_started_at) VALUES (1, ?, ?, ?, ?, ?)
|
|
136
220
|
`).run(record.pid, record.ownerId, record.ownerToken, record.bootIdentity, record.processStartedAt);
|
|
@@ -161,6 +245,7 @@ function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt)
|
|
|
161
245
|
}
|
|
162
246
|
if (!differentBoot && !differentProcess && isProcessAlive(observed.pid))
|
|
163
247
|
return { acquired: false, stale: null };
|
|
248
|
+
checkConfinement(confinement);
|
|
164
249
|
const changed = database.prepare(`
|
|
165
250
|
UPDATE session_turn_lease SET pid = ?, owner_id = ?, owner_token = ?, boot_identity = ?, process_started_at = ?
|
|
166
251
|
WHERE singleton = 1 AND pid = ? AND owner_id = ? AND owner_token = ?
|
|
@@ -174,7 +259,7 @@ function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt)
|
|
|
174
259
|
}
|
|
175
260
|
}
|
|
176
261
|
function releaseLeaseRecord(lockPath, record) {
|
|
177
|
-
const database = openLeaseDatabase(lockPath, 5_000);
|
|
262
|
+
const database = openLeaseDatabase(lockPath, 5_000, record.confinement);
|
|
178
263
|
try {
|
|
179
264
|
return database.prepare(`
|
|
180
265
|
DELETE FROM session_turn_lease WHERE singleton = 1 AND pid = ? AND owner_id = ? AND owner_token = ?
|
|
@@ -187,27 +272,42 @@ function releaseLeaseRecord(lockPath, record) {
|
|
|
187
272
|
}
|
|
188
273
|
function makeLease(held) {
|
|
189
274
|
let localReleased = false;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (held.depth > 0 || held.released)
|
|
200
|
-
return;
|
|
201
|
-
held.released = true;
|
|
275
|
+
const release = () => {
|
|
276
|
+
if (localReleased)
|
|
277
|
+
return;
|
|
278
|
+
localReleased = true;
|
|
279
|
+
held.depth -= 1;
|
|
280
|
+
if (held.depth > 0 || held.released)
|
|
281
|
+
return;
|
|
282
|
+
held.released = true;
|
|
283
|
+
try {
|
|
202
284
|
releaseLeaseRecord(held.lockPath, held);
|
|
203
|
-
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
204
287
|
(0, runtime_1.emitNervesEvent)({
|
|
205
|
-
component: "mind",
|
|
206
|
-
|
|
207
|
-
message: "released session turn lease",
|
|
288
|
+
level: "warn", component: "mind", event: "mind.session_turn_lease_release_failed",
|
|
289
|
+
message: "durable lease release failed; original recovery evidence retained",
|
|
208
290
|
meta: { sessionPath: held.sessionPath, ownerId: held.ownerId },
|
|
209
291
|
});
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
if (heldLeases.get(held.sessionPath) === held)
|
|
296
|
+
heldLeases.delete(held.sessionPath);
|
|
297
|
+
}
|
|
298
|
+
(0, runtime_1.emitNervesEvent)({
|
|
299
|
+
component: "mind",
|
|
300
|
+
event: "mind.session_turn_lease_released",
|
|
301
|
+
message: "released session turn lease",
|
|
302
|
+
meta: { sessionPath: held.sessionPath, ownerId: held.ownerId },
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
lease: {
|
|
307
|
+
sessionPath: held.sessionPath, ownerId: held.ownerId, ownerToken: held.ownerToken,
|
|
308
|
+
release: async () => release(),
|
|
210
309
|
},
|
|
310
|
+
release,
|
|
211
311
|
};
|
|
212
312
|
}
|
|
213
313
|
async function acquireSessionTurnLease(sessionPath, options = {}) {
|
|
@@ -215,10 +315,12 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
|
|
|
215
315
|
const existing = heldLeases.get(canonical);
|
|
216
316
|
if (existing && !existing.released) {
|
|
217
317
|
if (options.ownerId === existing.ownerId && options.ownerToken === existing.ownerToken) {
|
|
318
|
+
retainConfinement(existing, options.confinementRoot);
|
|
218
319
|
existing.depth += 1;
|
|
219
|
-
return makeLease(existing);
|
|
320
|
+
return makeLease(existing).lease;
|
|
220
321
|
}
|
|
221
322
|
}
|
|
323
|
+
const confinement = pinConfinement(canonical, options.confinementRoot);
|
|
222
324
|
const record = acquisitionRecord(options);
|
|
223
325
|
const { ownerId } = record;
|
|
224
326
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
@@ -226,18 +328,19 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
|
|
|
226
328
|
const isProcessAlive = options.isProcessAlive ?? processAlive;
|
|
227
329
|
const lockPath = `${canonical}.turn.lock`;
|
|
228
330
|
const started = Date.now();
|
|
331
|
+
checkConfinement(confinement);
|
|
229
332
|
fs.mkdirSync(path.dirname(canonical), { recursive: true });
|
|
230
333
|
for (;;) {
|
|
231
334
|
let claim = null;
|
|
232
335
|
try {
|
|
233
|
-
claim = claimLeaseRecord(lockPath, record, isProcessAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt);
|
|
336
|
+
claim = claimLeaseRecord(lockPath, record, isProcessAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt, confinement);
|
|
234
337
|
}
|
|
235
338
|
catch (error) {
|
|
236
339
|
if (!isSqliteBusy(error))
|
|
237
340
|
throw error;
|
|
238
341
|
}
|
|
239
342
|
if (claim?.acquired) {
|
|
240
|
-
const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false };
|
|
343
|
+
const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false, confinement };
|
|
241
344
|
heldLeases.set(canonical, held);
|
|
242
345
|
(0, runtime_1.emitNervesEvent)({
|
|
243
346
|
component: "mind",
|
|
@@ -255,7 +358,7 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
|
|
|
255
358
|
meta: { sessionPath: canonical, stalePid: claim.stale.pid, staleOwnerId: claim.stale.ownerId },
|
|
256
359
|
});
|
|
257
360
|
}
|
|
258
|
-
return makeLease(held);
|
|
361
|
+
return makeLease(held).lease;
|
|
259
362
|
}
|
|
260
363
|
if (Date.now() - started >= timeoutMs) {
|
|
261
364
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -272,25 +375,43 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
|
|
|
272
375
|
}
|
|
273
376
|
async function withSessionTurnLease(sessionPath, work, options = {}) {
|
|
274
377
|
const lease = await acquireSessionTurnLease(sessionPath, options);
|
|
378
|
+
let failed = false;
|
|
275
379
|
try {
|
|
276
380
|
return await leaseContext.run(lease, () => work(lease));
|
|
277
381
|
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
failed = true;
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
278
386
|
finally {
|
|
279
|
-
|
|
387
|
+
try {
|
|
388
|
+
await lease.release();
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (!failed)
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
280
394
|
}
|
|
281
395
|
}
|
|
282
396
|
function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
|
|
283
397
|
const canonical = canonicalSessionPath(sessionPath);
|
|
284
398
|
const contextual = currentSessionTurnLease(canonical);
|
|
285
|
-
if (contextual)
|
|
399
|
+
if (contextual) {
|
|
400
|
+
const held = heldLeases.get(canonical);
|
|
401
|
+
if (!held || held.released)
|
|
402
|
+
throw new SessionTransactionError("session lease owner token mismatch");
|
|
403
|
+
retainConfinement(held, options.confinementRoot);
|
|
286
404
|
return work(contextual);
|
|
405
|
+
}
|
|
406
|
+
const confinement = pinConfinement(canonical, options.confinementRoot);
|
|
287
407
|
const record = acquisitionRecord(options);
|
|
288
408
|
const lockPath = `${canonical}.turn.lock`;
|
|
409
|
+
checkConfinement(confinement);
|
|
289
410
|
fs.mkdirSync(path.dirname(canonical), { recursive: true });
|
|
290
411
|
const tryAcquire = () => {
|
|
291
412
|
let claim;
|
|
292
413
|
try {
|
|
293
|
-
claim = claimLeaseRecord(lockPath, record, options.isProcessAlive ?? processAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt);
|
|
414
|
+
claim = claimLeaseRecord(lockPath, record, options.isProcessAlive ?? processAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt, confinement);
|
|
294
415
|
}
|
|
295
416
|
catch (error) {
|
|
296
417
|
if (isSqliteBusy(error))
|
|
@@ -298,7 +419,7 @@ function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
|
|
|
298
419
|
throw error;
|
|
299
420
|
}
|
|
300
421
|
if (claim.acquired) {
|
|
301
|
-
const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false };
|
|
422
|
+
const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false, confinement };
|
|
302
423
|
heldLeases.set(canonical, held);
|
|
303
424
|
if (claim.stale) {
|
|
304
425
|
options.onStaleLease?.(claim.stale);
|
|
@@ -317,12 +438,23 @@ function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
|
|
|
317
438
|
const held = tryAcquire();
|
|
318
439
|
if (!held)
|
|
319
440
|
throw new SessionTurnBusyError(`session turn busy: ${canonical}`);
|
|
320
|
-
const lease = makeLease(held);
|
|
441
|
+
const { lease, release } = makeLease(held);
|
|
442
|
+
let failed = false;
|
|
321
443
|
try {
|
|
322
444
|
return leaseContext.run(lease, () => work(lease));
|
|
323
445
|
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
failed = true;
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
324
450
|
finally {
|
|
325
|
-
|
|
451
|
+
try {
|
|
452
|
+
release();
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (!failed)
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
326
458
|
}
|
|
327
459
|
}
|
|
328
460
|
function currentSessionTurnLease(sessionPath) {
|
|
@@ -337,6 +469,7 @@ function assertSessionTurnLease(sessionPath, lease) {
|
|
|
337
469
|
if (!held || held.released || held.ownerId !== lease.ownerId || held.ownerToken !== lease.ownerToken) {
|
|
338
470
|
throw new SessionTransactionError("session lease owner token mismatch");
|
|
339
471
|
}
|
|
472
|
+
checkConfinement(held.confinement);
|
|
340
473
|
}
|
|
341
474
|
function readSessionTransaction(sessionPath, lease) {
|
|
342
475
|
assertSessionTurnLease(sessionPath, lease);
|
|
@@ -353,6 +486,7 @@ function readSessionTransaction(sessionPath, lease) {
|
|
|
353
486
|
function writeSessionTransaction(sessionPath, value, options) {
|
|
354
487
|
assertSessionTurnLease(sessionPath, options.lease);
|
|
355
488
|
const canonical = canonicalSessionPath(sessionPath);
|
|
489
|
+
const confinement = heldLeases.get(canonical).confinement;
|
|
356
490
|
const current = readSessionTransaction(canonical, options.lease);
|
|
357
491
|
if (current.revision !== options.expectedRevision)
|
|
358
492
|
throw new SessionTransactionError("session revision changed");
|
|
@@ -360,14 +494,20 @@ function writeSessionTransaction(sessionPath, value, options) {
|
|
|
360
494
|
const bytes = JSON.stringify(value, null, 2);
|
|
361
495
|
const tempPath = path.join(path.dirname(canonical), `.${path.basename(canonical)}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`);
|
|
362
496
|
let fd = null;
|
|
497
|
+
let temporaryIdentity = null;
|
|
363
498
|
try {
|
|
364
499
|
fd = fs.openSync(tempPath, "wx", 0o600);
|
|
500
|
+
if (confinement)
|
|
501
|
+
temporaryIdentity = fs.fstatSync(fd);
|
|
365
502
|
fs.writeFileSync(fd, bytes, "utf8");
|
|
366
503
|
fs.fsyncSync(fd);
|
|
367
504
|
fs.closeSync(fd);
|
|
368
505
|
fd = null;
|
|
369
506
|
options.hooks?.beforeRename?.();
|
|
507
|
+
if (confinement)
|
|
508
|
+
checkTemporary(confinement, tempPath, temporaryIdentity);
|
|
370
509
|
fs.renameSync(tempPath, canonical);
|
|
510
|
+
checkConfinement(confinement);
|
|
371
511
|
const directoryFd = fs.openSync(path.dirname(canonical), "r");
|
|
372
512
|
try {
|
|
373
513
|
fs.fsyncSync(directoryFd);
|
|
@@ -384,9 +524,18 @@ function writeSessionTransaction(sessionPath, value, options) {
|
|
|
384
524
|
}
|
|
385
525
|
catch { /* best effort */ }
|
|
386
526
|
try {
|
|
527
|
+
if (confinement)
|
|
528
|
+
checkTemporary(confinement, tempPath, temporaryIdentity);
|
|
387
529
|
fs.unlinkSync(tempPath);
|
|
388
530
|
}
|
|
389
|
-
catch {
|
|
531
|
+
catch {
|
|
532
|
+
if (confinement)
|
|
533
|
+
(0, runtime_1.emitNervesEvent)({
|
|
534
|
+
level: "warn", component: "mind", event: "mind.session_transaction_cleanup_refused",
|
|
535
|
+
message: "temporary cleanup unavailable; original evidence retained",
|
|
536
|
+
meta: { sessionPath: canonical },
|
|
537
|
+
});
|
|
538
|
+
}
|
|
390
539
|
throw error;
|
|
391
540
|
}
|
|
392
541
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -253,7 +253,7 @@ function causalSessionEventIds(view, attempts, finalAttemptIndex, finalCoordinat
|
|
|
253
253
|
function currentIngressEventId(events, existingEventIds, userMessage, precommittedIngress, ingressRelations) {
|
|
254
254
|
const reference = ingressRelations?.references[0];
|
|
255
255
|
const carriesReference = (event, expected) => Array.isArray(event.relations?.references) && event.relations.references.includes(expected);
|
|
256
|
-
const matches = events.filter((event) => (event.role === "user"
|
|
256
|
+
const matches = (0, session_events_1.selectEffectiveSessionEvents)(events).filter((event) => (event.role === "user"
|
|
257
257
|
&& event.content === userMessage
|
|
258
258
|
&& event.provenance?.captureKind === "live"
|
|
259
259
|
&& (precommittedIngress
|
|
@@ -273,6 +273,7 @@ function exactProjectedIngressMessage(existing, messages, eventId) {
|
|
|
273
273
|
if (eventsById.size !== existing.events.length)
|
|
274
274
|
return null;
|
|
275
275
|
const seenProjectionIds = new Set();
|
|
276
|
+
const effectiveIds = new Set((0, session_events_1.selectEffectiveSessionEvents)(existing.events).map((event) => event.id));
|
|
276
277
|
const projectedEvents = [];
|
|
277
278
|
for (const projectedId of existing.projectionEventIds) {
|
|
278
279
|
if (typeof projectedId !== "string" || !projectedId.trim() || seenProjectionIds.has(projectedId))
|
|
@@ -281,7 +282,8 @@ function exactProjectedIngressMessage(existing, messages, eventId) {
|
|
|
281
282
|
if (!event)
|
|
282
283
|
return null;
|
|
283
284
|
seenProjectionIds.add(projectedId);
|
|
284
|
-
|
|
285
|
+
if (effectiveIds.has(event.id))
|
|
286
|
+
projectedEvents.push(event);
|
|
285
287
|
}
|
|
286
288
|
if (existing.projectionEventIds.filter((projectedId) => projectedId === eventId).length !== 1)
|
|
287
289
|
return null;
|
|
@@ -404,7 +406,7 @@ async function runSenseTurnExclusive(options) {
|
|
|
404
406
|
? existing?.events?.find((candidate) => candidate.id === options.precommittedIngress.eventId)
|
|
405
407
|
: undefined;
|
|
406
408
|
if (options.precommittedIngress) {
|
|
407
|
-
const latestUserEvent = existing?.events
|
|
409
|
+
const latestUserEvent = (0, session_events_1.selectEffectiveSessionEvents)(existing?.events ?? []).filter((candidate) => candidate.role === "user").at(-1);
|
|
408
410
|
if (!precommittedIngressEvent || precommittedIngressEvent !== latestUserEvent || precommittedIngressEvent.role !== "user" || precommittedIngressEvent.content !== userMessage
|
|
409
411
|
|| !precommittedIngressEvent.relations.references.includes(options.precommittedIngress.reference)) {
|
|
410
412
|
throw new Error("shared turn precommitted ingress is missing, mismatched, or no longer current");
|