dist / memory / processing / pendingQueue.js
dist / memory / processing / pendingQueue.js
"use strict";
/**
* @file processing/pendingQueue.ts
* C1: crash-safe queue of pending AI extractions (fixes tension T4 โ
* "fact is lost when the process restarts mid-extraction").
*
* Every extraction is registered in an append-style JSONL file BEFORE it
* starts. Completed entries are removed by atomic rewrite (tmp+rename).
* The next preprocessor run drains what survived โ i.e. facts whose
* extraction died with the process โ and stores them on the next boot.
*
* The in-process Map is the source of truth; the JSONL file is the bridge
* across restarts. All fs errors are swallowed โ the queue must never
* break a chat turn (resilience-by-design, Constitution ยง3.1 analogue).
* Entries expire after PENDING_QUEUE_MAX_AGE_MS and die after
* PENDING_QUEUE_MAX_ATTEMPTS failed attempts (poison messages).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PendingFactsQueue = void 0;
exports.getPendingQueue = getPendingQueue;
exports.resetPendingQueue = resetPendingQueue;
const fs_1 = require("fs/promises");
const path_1 = require("path");
const os_1 = require("os");
const crypto_1 = require("crypto");
const constants_1 = require("../constants");
/** Same directory as memory.db โ the plugin's persistent state area. */
const DEFAULT_FILE = (0, path_1.join)((0, os_1.homedir)(), ".lmstudio", "plugin-data", "persistent-memory", "pending-facts.jsonl");
class PendingFactsQueue {
constructor(filePath = DEFAULT_FILE) {
this.path = filePath;
/** id โ entry; source of truth within the process. */
this.entries = new Map();
this.loaded = false;
/** Serialization chain โ all file mutations run strictly in order. */
this._queue = Promise.resolve();
}
/** Redirect (tests / storage path changes); forgets the loaded state. */
setPath(p) {
this.path = p;
this.entries.clear();
this.loaded = false;
}
/** Await all queued operations (test helper / shutdown). */
ready() {
return this._queue;
}
/** Register an extraction. Returns the id synchronously. */
enqueue(entry) {
const id = (0, crypto_1.randomUUID)();
const rec = { id, ts: Date.now(), attempts: 0, text: String(entry.text ?? "").slice(0, 2000), existingSummary: String(entry.existingSummary ?? "") };
this._queue = this._queue
.then(() => this._ensureLoaded())
.then(() => {
this.entries.set(id, rec);
this._trimOldest();
return this._persist();
})
.catch(() => { });
return id;
}
/** Oldest-first snapshot of retryable entries. */
async pending(limit = constants_1.PENDING_QUEUE_DRAIN_LIMIT, excludeIds = new Set()) {
await this._chain(() => this._ensureLoaded());
const now = Date.now();
return [...this.entries.values()]
.filter((e) => e.ts >= now - constants_1.PENDING_QUEUE_MAX_AGE_MS
&& e.attempts < constants_1.PENDING_QUEUE_MAX_ATTEMPTS
&& !excludeIds.has(e.id))
.sort((a, b) => a.ts - b.ts)
.slice(0, limit);
}
/** Extraction finished (facts stored or genuinely none) โ drop entry. */
complete(id) {
return this._chain(async () => {
await this._ensureLoaded();
if (this.entries.delete(id))
await this._persist();
});
}
/** Extraction failed โ count attempt; drop after max attempts. */
fail(id) {
return this._chain(async () => {
await this._ensureLoaded();
const e = this.entries.get(id);
if (!e)
return;
e.attempts++;
if (e.attempts >= constants_1.PENDING_QUEUE_MAX_ATTEMPTS)
this.entries.delete(id);
await this._persist();
});
}
async _chain(task) {
const p = this._queue.then(task).catch(() => { });
this._queue = p;
return p;
}
_trimOldest() {
while (this.entries.size > constants_1.PENDING_QUEUE_MAX_ENTRIES) {
let oldest = null;
for (const e of this.entries.values()) {
if (!oldest || e.ts < oldest.ts)
oldest = e;
}
if (!oldest)
break;
this.entries.delete(oldest.id);
}
}
async _ensureLoaded() {
if (this.loaded)
return;
this.loaded = true;
try {
const raw = await (0, fs_1.readFile)(this.path, "utf8");
const now = Date.now();
for (const line of raw.split("\n")) {
const t = line.trim();
if (!t)
continue;
try {
const e = JSON.parse(t);
if (typeof e.id !== "string" || typeof e.text !== "string")
continue;
if (e.ts < now - constants_1.PENDING_QUEUE_MAX_AGE_MS)
continue; // stale โ the moment has passed
this.entries.set(e.id, { attempts: 0, existingSummary: "", ...e });
}
catch { /* corrupt line โ skip */ }
}
}
catch { /* no file yet */ }
}
/** Atomic rewrite: tmp + rename, serialized by the chain. */
async _persist() {
try {
await (0, fs_1.mkdir)((0, path_1.dirname)(this.path), { recursive: true });
const body = [...this.entries.values()].map((e) => JSON.stringify(e)).join("\n");
const tmp = this.path + ".tmp";
await (0, fs_1.writeFile)(tmp, body ? body + "\n" : "", "utf8");
await (0, fs_1.rename)(tmp, this.path);
}
catch { /* fs unavailable โ in-memory copy still serves the session */ }
}
}
exports.PendingFactsQueue = PendingFactsQueue;
let singleton = null;
/** Shared instance (module-scoped โ survives toolsProvider re-invocations). */
function getPendingQueue() {
if (!singleton)
singleton = new PendingFactsQueue();
return singleton;
}
/** Drop the singleton (tests). */
function resetPendingQueue() {
singleton = null;
}
//# sourceMappingURL=pendingQueue.js.map
"use strict";
/**
* @file processing/pendingQueue.ts
* C1: crash-safe queue of pending AI extractions (fixes tension T4 โ
* "fact is lost when the process restarts mid-extraction").
*
* Every extraction is registered in an append-style JSONL file BEFORE it
* starts. Completed entries are removed by atomic rewrite (tmp+rename).
* The next preprocessor run drains what survived โ i.e. facts whose
* extraction died with the process โ and stores them on the next boot.
*
* The in-process Map is the source of truth; the JSONL file is the bridge
* across restarts. All fs errors are swallowed โ the queue must never
* break a chat turn (resilience-by-design, Constitution ยง3.1 analogue).
* Entries expire after PENDING_QUEUE_MAX_AGE_MS and die after
* PENDING_QUEUE_MAX_ATTEMPTS failed attempts (poison messages).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PendingFactsQueue = void 0;
exports.getPendingQueue = getPendingQueue;
exports.resetPendingQueue = resetPendingQueue;
const fs_1 = require("fs/promises");
const path_1 = require("path");
const os_1 = require("os");
const crypto_1 = require("crypto");
const constants_1 = require("../constants");
/** Same directory as memory.db โ the plugin's persistent state area. */
const DEFAULT_FILE = (0, path_1.join)((0, os_1.homedir)(), ".lmstudio", "plugin-data", "persistent-memory", "pending-facts.jsonl");
class PendingFactsQueue {
constructor(filePath = DEFAULT_FILE) {
this.path = filePath;
/** id โ entry; source of truth within the process. */
this.entries = new Map();
this.loaded = false;
/** Serialization chain โ all file mutations run strictly in order. */
this._queue = Promise.resolve();
}
/** Redirect (tests / storage path changes); forgets the loaded state. */
setPath(p) {
this.path = p;
this.entries.clear();
this.loaded = false;
}
/** Await all queued operations (test helper / shutdown). */
ready() {
return this._queue;
}
/** Register an extraction. Returns the id synchronously. */
enqueue(entry) {
const id = (0, crypto_1.randomUUID)();
const rec = { id, ts: Date.now(), attempts: 0, text: String(entry.text ?? "").slice(0, 2000), existingSummary: String(entry.existingSummary ?? "") };
this._queue = this._queue
.then(() => this._ensureLoaded())
.then(() => {
this.entries.set(id, rec);
this._trimOldest();
return this._persist();
})
.catch(() => { });
return id;
}
/** Oldest-first snapshot of retryable entries. */
async pending(limit = constants_1.PENDING_QUEUE_DRAIN_LIMIT, excludeIds = new Set()) {
await this._chain(() => this._ensureLoaded());
const now = Date.now();
return [...this.entries.values()]
.filter((e) => e.ts >= now - constants_1.PENDING_QUEUE_MAX_AGE_MS
&& e.attempts < constants_1.PENDING_QUEUE_MAX_ATTEMPTS
&& !excludeIds.has(e.id))
.sort((a, b) => a.ts - b.ts)
.slice(0, limit);
}
/** Extraction finished (facts stored or genuinely none) โ drop entry. */
complete(id) {
return this._chain(async () => {
await this._ensureLoaded();
if (this.entries.delete(id))
await this._persist();
});
}
/** Extraction failed โ count attempt; drop after max attempts. */
fail(id) {
return this._chain(async () => {
await this._ensureLoaded();
const e = this.entries.get(id);
if (!e)
return;
e.attempts++;
if (e.attempts >= constants_1.PENDING_QUEUE_MAX_ATTEMPTS)
this.entries.delete(id);
await this._persist();
});
}
async _chain(task) {
const p = this._queue.then(task).catch(() => { });
this._queue = p;
return p;
}
_trimOldest() {
while (this.entries.size > constants_1.PENDING_QUEUE_MAX_ENTRIES) {
let oldest = null;
for (const e of this.entries.values()) {
if (!oldest || e.ts < oldest.ts)
oldest = e;
}
if (!oldest)
break;
this.entries.delete(oldest.id);
}
}
async _ensureLoaded() {
if (this.loaded)
return;
this.loaded = true;
try {
const raw = await (0, fs_1.readFile)(this.path, "utf8");
const now = Date.now();
for (const line of raw.split("\n")) {
const t = line.trim();
if (!t)
continue;
try {
const e = JSON.parse(t);
if (typeof e.id !== "string" || typeof e.text !== "string")
continue;
if (e.ts < now - constants_1.PENDING_QUEUE_MAX_AGE_MS)
continue; // stale โ the moment has passed
this.entries.set(e.id, { attempts: 0, existingSummary: "", ...e });
}
catch { /* corrupt line โ skip */ }
}
}
catch { /* no file yet */ }
}
/** Atomic rewrite: tmp + rename, serialized by the chain. */
async _persist() {
try {
await (0, fs_1.mkdir)((0, path_1.dirname)(this.path), { recursive: true });
const body = [...this.entries.values()].map((e) => JSON.stringify(e)).join("\n");
const tmp = this.path + ".tmp";
await (0, fs_1.writeFile)(tmp, body ? body + "\n" : "", "utf8");
await (0, fs_1.rename)(tmp, this.path);
}
catch { /* fs unavailable โ in-memory copy still serves the session */ }
}
}
exports.PendingFactsQueue = PendingFactsQueue;
let singleton = null;
/** Shared instance (module-scoped โ survives toolsProvider re-invocations). */
function getPendingQueue() {
if (!singleton)
singleton = new PendingFactsQueue();
return singleton;
}
/** Drop the singleton (tests). */
function resetPendingQueue() {
singleton = null;
}
//# sourceMappingURL=pendingQueue.js.map