@rubytech/create-realagent 1.0.665 → 1.0.666

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.665",
3
+ "version": "1.0.666",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -308,9 +308,19 @@ OPTIONS {
308
308
  // Linked via (Message)-[:NEXT]->(Message) to the next message in
309
309
  // insertion order (Task 621). The chain is linear — one outgoing
310
310
  // NEXT per message — so a Conversation with N messages has N-1
311
- // NEXT edges. Concurrent writes against the same Conversation
312
- // CAN fork the chain under Neo4j's READ_COMMITTED default; tracked
313
- // under Task 624.
311
+ // NEXT edges.
312
+ //
313
+ // Chain-fork invariant (Task 624): the write path in
314
+ // platform/ui/app/lib/neo4j-store.ts#persistMessage holds a
315
+ // per-conversationId async mutex around the Cypher that observes
316
+ // the tail and CREATEs the new [:NEXT] edge. Without this, two
317
+ // concurrent writes under READ_COMMITTED both see the same tail
318
+ // and each CREATE an outgoing NEXT → two edges from one node.
319
+ // Scope: single node-process only; multi-replica coordination is
320
+ // a follow-up (see .docs/neo4j.md). Any new write path that creates
321
+ // Message nodes on the chain (bulk import, migration, rehydrate)
322
+ // MUST go through persistMessage or hold an equivalent lock.
323
+ //
314
324
  // Vector-indexed for semantic search over conversation history.
315
325
  // ----------------------------------------------------------
316
326
 
@@ -5281,6 +5281,7 @@ async function persistToolCall(record) {
5281
5281
  }
5282
5282
  }
5283
5283
  var SUMMARY_MAX_LEN = 200;
5284
+ var persistMessageLocks = /* @__PURE__ */ new Map();
5284
5285
  async function persistMessage(conversationId, role, content, accountId, tokens, createdAt, sender) {
5285
5286
  if (!content) return null;
5286
5287
  const messageId = randomUUID();
@@ -5291,6 +5292,15 @@ async function persistMessage(conversationId, role, content, accountId, tokens,
5291
5292
  } catch (err) {
5292
5293
  console.error(`[persist] Embedding failed, storing without: ${err instanceof Error ? err.message : String(err)}`);
5293
5294
  }
5295
+ const prev = persistMessageLocks.get(conversationId);
5296
+ const waited = prev !== void 0;
5297
+ let release;
5298
+ const mine = new Promise((resolve31) => {
5299
+ release = resolve31;
5300
+ });
5301
+ const chained = (prev ?? Promise.resolve()).then(() => mine);
5302
+ persistMessageLocks.set(conversationId, chained);
5303
+ await prev;
5294
5304
  const session = getSession();
5295
5305
  try {
5296
5306
  const result = await session.run(
@@ -5353,7 +5363,7 @@ async function persistMessage(conversationId, role, content, accountId, tokens,
5353
5363
  const summarySetThisWrite = record.get("summarySetThisWrite") === true;
5354
5364
  const chainLenRaw = record.get("chainLen");
5355
5365
  const chainLen = typeof chainLenRaw === "bigint" ? Number(chainLenRaw) : typeof chainLenRaw?.toNumber === "function" ? chainLenRaw.toNumber() : Number(chainLenRaw ?? 0);
5356
- console.error(`[neo4j-store] append-message conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 prev=${prevMessageId ? prevMessageId.slice(0, 8) + "\u2026" : "null"} chainLen=${chainLen}`);
5366
+ console.error(`[neo4j-store] append-message conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 prev=${prevMessageId ? prevMessageId.slice(0, 8) + "\u2026" : "null"} chainLen=${chainLen} waited=${waited}`);
5357
5367
  if (summarySetThisWrite) {
5358
5368
  console.error(`[neo4j-store] conversation-summary-set conversationId=${conversationId.slice(0, 8)}\u2026 len=${summary.length}`);
5359
5369
  }
@@ -5363,6 +5373,10 @@ async function persistMessage(conversationId, role, content, accountId, tokens,
5363
5373
  console.error(`[persist] Neo4j write failed: ${err instanceof Error ? err.message : String(err)}`);
5364
5374
  return null;
5365
5375
  } finally {
5376
+ release();
5377
+ if (persistMessageLocks.get(conversationId) === chained) {
5378
+ persistMessageLocks.delete(conversationId);
5379
+ }
5366
5380
  await session.close();
5367
5381
  }
5368
5382
  }