@agent-diaries/core 1.1.0 → 1.1.2
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/src/adapters/mongo.d.ts +16 -0
- package/dist/src/adapters/mongo.js +73 -0
- package/dist/src/adapters/redis.d.ts +14 -0
- package/dist/src/adapters/redis.js +64 -0
- package/dist/src/adapters/sqlite.d.ts +16 -0
- package/dist/src/adapters/sqlite.js +126 -0
- package/dist/src/diary.d.ts +59 -0
- package/dist/src/diary.js +116 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +18 -0
- package/dist/src/storage.d.ts +18 -0
- package/dist/src/storage.js +95 -0
- package/package.json +9 -11
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StorageAdapter } from '../storage';
|
|
2
|
+
import { Collection } from 'mongodb';
|
|
3
|
+
interface MongoStorageOptions {
|
|
4
|
+
collection: Collection;
|
|
5
|
+
}
|
|
6
|
+
export declare class MongoStorage<T> implements StorageAdapter<T> {
|
|
7
|
+
private collection;
|
|
8
|
+
private static indexedCollections;
|
|
9
|
+
constructor(options: MongoStorageOptions);
|
|
10
|
+
private ensureIndex;
|
|
11
|
+
private hashString;
|
|
12
|
+
get(key: string): Promise<T | null>;
|
|
13
|
+
set(key: string, data: T): Promise<void>;
|
|
14
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MongoStorage = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
class MongoStorage {
|
|
9
|
+
collection;
|
|
10
|
+
static indexedCollections = new Set();
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.collection = options.collection;
|
|
13
|
+
}
|
|
14
|
+
async ensureIndex() {
|
|
15
|
+
const ns = this.collection.namespace;
|
|
16
|
+
if (MongoStorage.indexedCollections.has(ns))
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
await this.collection.createIndex({ lockedAt: 1 }, { expireAfterSeconds: 10, partialFilterExpression: { lockedAt: { $exists: true } } });
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.warn('[MongoStorage] Failed to create TTL index:', e);
|
|
23
|
+
}
|
|
24
|
+
MongoStorage.indexedCollections.add(ns);
|
|
25
|
+
}
|
|
26
|
+
hashString(str) {
|
|
27
|
+
return crypto_1.default.createHash('sha256').update(str).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
async get(key) {
|
|
30
|
+
const hash = this.hashString(key);
|
|
31
|
+
const doc = await this.collection.findOne({ _id: hash });
|
|
32
|
+
if (!doc || !doc.data)
|
|
33
|
+
return null;
|
|
34
|
+
return JSON.parse(doc.data);
|
|
35
|
+
}
|
|
36
|
+
async set(key, data) {
|
|
37
|
+
const hash = this.hashString(key);
|
|
38
|
+
await this.collection.updateOne({ _id: hash }, { $set: { data: JSON.stringify(data) } }, { upsert: true });
|
|
39
|
+
}
|
|
40
|
+
async withLock(key, fn) {
|
|
41
|
+
await this.ensureIndex();
|
|
42
|
+
const hash = this.hashString(key);
|
|
43
|
+
const lockId = `lock:${hash}`;
|
|
44
|
+
const acquireLock = async () => {
|
|
45
|
+
try {
|
|
46
|
+
await this.collection.insertOne({ _id: lockId, lockedAt: new Date() });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code === 11000)
|
|
51
|
+
return false;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
let attempt = 0;
|
|
56
|
+
while (!(await acquireLock())) {
|
|
57
|
+
const backoff = Math.min(10 * Math.pow(2, attempt), 500);
|
|
58
|
+
const jitter = Math.random() * 50;
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, backoff + jitter));
|
|
60
|
+
attempt++;
|
|
61
|
+
if (attempt > 150) {
|
|
62
|
+
throw new Error(`[MongoStorage] Lock timeout on key: ${key}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return await fn();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await this.collection.deleteOne({ _id: lockId });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.MongoStorage = MongoStorage;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StorageAdapter } from '../storage';
|
|
2
|
+
import Redis from 'ioredis';
|
|
3
|
+
export declare class RedisStorage<T> implements StorageAdapter<T> {
|
|
4
|
+
private redis;
|
|
5
|
+
private prefix;
|
|
6
|
+
constructor(options: {
|
|
7
|
+
redis: Redis;
|
|
8
|
+
prefix?: string;
|
|
9
|
+
});
|
|
10
|
+
private getKey;
|
|
11
|
+
get(key: string): Promise<T | null>;
|
|
12
|
+
set(key: string, value: T): Promise<void>;
|
|
13
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisStorage = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
class RedisStorage {
|
|
6
|
+
redis;
|
|
7
|
+
prefix;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.redis = options.redis;
|
|
10
|
+
this.prefix = options.prefix || 'agent-diaries:';
|
|
11
|
+
}
|
|
12
|
+
getKey(key) {
|
|
13
|
+
return `${this.prefix}${key}`;
|
|
14
|
+
}
|
|
15
|
+
async get(key) {
|
|
16
|
+
const data = await this.redis.get(this.getKey(key));
|
|
17
|
+
if (!data)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(data);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.error(`[RedisStorage] Failed to parse JSON for key ${key}`);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async set(key, value) {
|
|
28
|
+
const data = JSON.stringify(value);
|
|
29
|
+
await this.redis.set(this.getKey(key), data);
|
|
30
|
+
}
|
|
31
|
+
async withLock(key, fn) {
|
|
32
|
+
const lockKey = `${this.getKey(key)}:lock`;
|
|
33
|
+
const lockValue = (0, crypto_1.randomUUID)();
|
|
34
|
+
const lockTtlMs = 10000; // 10 seconds max lock
|
|
35
|
+
const acquireLock = async () => {
|
|
36
|
+
const result = await this.redis.set(lockKey, lockValue, 'PX', lockTtlMs, 'NX');
|
|
37
|
+
return result === 'OK';
|
|
38
|
+
};
|
|
39
|
+
let attempt = 0;
|
|
40
|
+
while (!(await acquireLock())) {
|
|
41
|
+
const backoff = Math.min(10 * Math.pow(2, attempt), 500);
|
|
42
|
+
const jitter = Math.random() * 50;
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, backoff + jitter));
|
|
44
|
+
attempt++;
|
|
45
|
+
if (attempt > 60)
|
|
46
|
+
throw new Error(`[RedisStorage] Lock timeout on key: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
// Safe release: ensure we only delete the lock if we still own it
|
|
53
|
+
const luaScript = `
|
|
54
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
55
|
+
return redis.call("del", KEYS[1])
|
|
56
|
+
else
|
|
57
|
+
return 0
|
|
58
|
+
end
|
|
59
|
+
`;
|
|
60
|
+
await this.redis.eval(luaScript, 1, lockKey, lockValue);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.RedisStorage = RedisStorage;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StorageAdapter } from '../storage';
|
|
2
|
+
import type { Database } from 'better-sqlite3';
|
|
3
|
+
export interface SqliteStorageOptions {
|
|
4
|
+
db: Database;
|
|
5
|
+
tableName?: string;
|
|
6
|
+
locksTableName?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class SqliteStorage<T> implements StorageAdapter<T> {
|
|
9
|
+
private db;
|
|
10
|
+
private tableName;
|
|
11
|
+
private locksTableName;
|
|
12
|
+
constructor(options: SqliteStorageOptions);
|
|
13
|
+
get(key: string): Promise<T | null>;
|
|
14
|
+
set(key: string, value: T): Promise<void>;
|
|
15
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SqliteStorage = void 0;
|
|
4
|
+
class SqliteStorage {
|
|
5
|
+
db;
|
|
6
|
+
tableName;
|
|
7
|
+
locksTableName;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
if (!options.db) {
|
|
10
|
+
throw new Error('[SqliteStorage] database instance (db) is required.');
|
|
11
|
+
}
|
|
12
|
+
this.db = options.db;
|
|
13
|
+
this.tableName = options.tableName || 'agent_diaries_storage';
|
|
14
|
+
this.locksTableName = options.locksTableName || 'agent_diaries_locks';
|
|
15
|
+
// Initialize tables synchronously as better-sqlite3 is fully synchronous
|
|
16
|
+
this.db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
18
|
+
key TEXT PRIMARY KEY,
|
|
19
|
+
value TEXT
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS ${this.locksTableName} (
|
|
22
|
+
key TEXT PRIMARY KEY,
|
|
23
|
+
locked_at INTEGER
|
|
24
|
+
);
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
async get(key) {
|
|
28
|
+
try {
|
|
29
|
+
const row = this.db
|
|
30
|
+
.prepare(`SELECT value FROM ${this.tableName} WHERE key = ?`)
|
|
31
|
+
.get(key);
|
|
32
|
+
if (!row)
|
|
33
|
+
return null;
|
|
34
|
+
return JSON.parse(row.value);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(`[SqliteStorage] Failed to get key ${key}:`, e);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async set(key, value) {
|
|
42
|
+
try {
|
|
43
|
+
const serialized = JSON.stringify(value);
|
|
44
|
+
this.db
|
|
45
|
+
.prepare(`
|
|
46
|
+
INSERT INTO ${this.tableName} (key, value)
|
|
47
|
+
VALUES (?, ?)
|
|
48
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
49
|
+
`)
|
|
50
|
+
.run(key, serialized);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
console.error(`[SqliteStorage] Failed to set key ${key}:`, e);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async withLock(key, fn) {
|
|
58
|
+
const lockKey = `lock:${key}`;
|
|
59
|
+
const lockTtlMs = 10000; // 10 seconds max lock
|
|
60
|
+
const acquireLock = () => {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
try {
|
|
63
|
+
this.db
|
|
64
|
+
.prepare(`INSERT INTO ${this.locksTableName} (key, locked_at) VALUES (?, ?)`)
|
|
65
|
+
.run(lockKey, now);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' ||
|
|
70
|
+
error.code === 'SQLITE_CONSTRAINT' ||
|
|
71
|
+
(error.message && error.message.includes('UNIQUE constraint failed'))) {
|
|
72
|
+
// Check if the lock has expired
|
|
73
|
+
const existing = this.db
|
|
74
|
+
.prepare(`SELECT locked_at FROM ${this.locksTableName} WHERE key = ?`)
|
|
75
|
+
.get(lockKey);
|
|
76
|
+
if (existing && now - existing.locked_at > lockTtlMs) {
|
|
77
|
+
// Attempt to clear the expired lock and acquire it
|
|
78
|
+
try {
|
|
79
|
+
// Wrap cleanup and acquire in a transaction for atomicity
|
|
80
|
+
const runCleanupAndAcquire = this.db.transaction(() => {
|
|
81
|
+
this.db
|
|
82
|
+
.prepare(`DELETE FROM ${this.locksTableName} WHERE key = ? AND locked_at = ?`)
|
|
83
|
+
.run(lockKey, existing.locked_at);
|
|
84
|
+
this.db
|
|
85
|
+
.prepare(`INSERT INTO ${this.locksTableName} (key, locked_at) VALUES (?, ?)`)
|
|
86
|
+
.run(lockKey, now);
|
|
87
|
+
});
|
|
88
|
+
runCleanupAndAcquire();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch (retryError) {
|
|
92
|
+
// Conflict during retry, lock is still held or acquired by another concurrent process
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
let attempt = 0;
|
|
102
|
+
while (!acquireLock()) {
|
|
103
|
+
const backoff = Math.min(10 * Math.pow(2, attempt), 500);
|
|
104
|
+
const jitter = Math.random() * 50;
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, backoff + jitter));
|
|
106
|
+
attempt++;
|
|
107
|
+
if (attempt > 60) {
|
|
108
|
+
throw new Error(`[SqliteStorage] Lock timeout on key: ${key}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return await fn();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
try {
|
|
116
|
+
this.db
|
|
117
|
+
.prepare(`DELETE FROM ${this.locksTableName} WHERE key = ?`)
|
|
118
|
+
.run(lockKey);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.error(`[SqliteStorage] Failed to release lock on key ${key}:`, e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.SqliteStorage = SqliteStorage;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { StorageAdapter } from './storage';
|
|
2
|
+
export interface TaskRecord {
|
|
3
|
+
title: string;
|
|
4
|
+
signature: string;
|
|
5
|
+
result?: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentState {
|
|
9
|
+
lastRun: number;
|
|
10
|
+
seenSignatures: string[];
|
|
11
|
+
runCount: number;
|
|
12
|
+
history: TaskRecord[];
|
|
13
|
+
}
|
|
14
|
+
export interface AgentDiaryOptions {
|
|
15
|
+
agentId: string;
|
|
16
|
+
storage?: StorageAdapter<AgentState>;
|
|
17
|
+
maxHistory?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class AgentDiary {
|
|
20
|
+
private agentId;
|
|
21
|
+
private storage;
|
|
22
|
+
private maxHistory;
|
|
23
|
+
constructor(options: AgentDiaryOptions);
|
|
24
|
+
private emptyState;
|
|
25
|
+
/**
|
|
26
|
+
* Generates a normalized signature for a task title.
|
|
27
|
+
*/
|
|
28
|
+
static normalizeSignature(title: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Reads the current diary state (without locking).
|
|
31
|
+
*/
|
|
32
|
+
readDiary(): Promise<AgentState>;
|
|
33
|
+
/**
|
|
34
|
+
* Atomically attempts to claim a task.
|
|
35
|
+
* Returns true if successfully claimed (first time seen), false if already claimed/processed.
|
|
36
|
+
*/
|
|
37
|
+
claimTask(title: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a task has already been processed by the agent.
|
|
40
|
+
*/
|
|
41
|
+
hasProcessedTask(title: string): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves the stored result of a previously processed task, if available.
|
|
44
|
+
*/
|
|
45
|
+
getTaskResult(title: string): Promise<string | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* Filters out items that the agent has already processed.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ WARNING: filterNewTasks() is a non-atomic snapshot. Always follow it with claimTask()
|
|
50
|
+
* to atomically claim ownership. Never act on filterNewTasks() results directly without claiming them first.
|
|
51
|
+
*/
|
|
52
|
+
filterNewTasks<T extends {
|
|
53
|
+
title: string;
|
|
54
|
+
}>(tasks: T[]): Promise<T[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Updates a claimed task with its final result.
|
|
57
|
+
*/
|
|
58
|
+
writeTaskResult(title: string, result?: string): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentDiary = void 0;
|
|
4
|
+
const storage_1 = require("./storage");
|
|
5
|
+
class AgentDiary {
|
|
6
|
+
agentId;
|
|
7
|
+
storage;
|
|
8
|
+
maxHistory;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.agentId = options.agentId;
|
|
11
|
+
// Use local file storage by default if none provided
|
|
12
|
+
this.storage = options.storage || new storage_1.LocalFileStorage();
|
|
13
|
+
this.maxHistory = options.maxHistory || 500;
|
|
14
|
+
}
|
|
15
|
+
emptyState() {
|
|
16
|
+
return {
|
|
17
|
+
lastRun: 0,
|
|
18
|
+
seenSignatures: [],
|
|
19
|
+
runCount: 0,
|
|
20
|
+
history: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generates a normalized signature for a task title.
|
|
25
|
+
*/
|
|
26
|
+
static normalizeSignature(title) {
|
|
27
|
+
return (title || '').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reads the current diary state (without locking).
|
|
31
|
+
*/
|
|
32
|
+
async readDiary() {
|
|
33
|
+
const state = await this.storage.get(`diary_${this.agentId}`);
|
|
34
|
+
return state ?? this.emptyState();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Atomically attempts to claim a task.
|
|
38
|
+
* Returns true if successfully claimed (first time seen), false if already claimed/processed.
|
|
39
|
+
*/
|
|
40
|
+
async claimTask(title) {
|
|
41
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
42
|
+
return await this.storage.withLock(`diary_${this.agentId}`, async () => {
|
|
43
|
+
const state = await this.readDiary();
|
|
44
|
+
const seenSet = new Set(state.seenSignatures);
|
|
45
|
+
if (seenSet.has(signature)) {
|
|
46
|
+
return false; // Task already exists
|
|
47
|
+
}
|
|
48
|
+
// Claim it immediately to prevent race conditions
|
|
49
|
+
const record = {
|
|
50
|
+
title,
|
|
51
|
+
signature,
|
|
52
|
+
timestamp: Date.now()
|
|
53
|
+
};
|
|
54
|
+
state.history = [record, ...state.history].slice(0, this.maxHistory);
|
|
55
|
+
// Fix Desync Bug: seenSignatures is strictly derived from the sliced history
|
|
56
|
+
state.seenSignatures = state.history.map(r => r.signature);
|
|
57
|
+
state.runCount += 1;
|
|
58
|
+
state.lastRun = Date.now();
|
|
59
|
+
await this.storage.set(`diary_${this.agentId}`, state);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks if a task has already been processed by the agent.
|
|
65
|
+
*/
|
|
66
|
+
async hasProcessedTask(title) {
|
|
67
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
68
|
+
const state = await this.readDiary();
|
|
69
|
+
const seenSet = new Set(state.seenSignatures);
|
|
70
|
+
return seenSet.has(signature);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves the stored result of a previously processed task, if available.
|
|
74
|
+
*/
|
|
75
|
+
async getTaskResult(title) {
|
|
76
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
77
|
+
const state = await this.readDiary();
|
|
78
|
+
const record = state.history.find(r => r.signature === signature);
|
|
79
|
+
return record?.result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Filters out items that the agent has already processed.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ WARNING: filterNewTasks() is a non-atomic snapshot. Always follow it with claimTask()
|
|
85
|
+
* to atomically claim ownership. Never act on filterNewTasks() results directly without claiming them first.
|
|
86
|
+
*/
|
|
87
|
+
async filterNewTasks(tasks) {
|
|
88
|
+
const state = await this.readDiary();
|
|
89
|
+
const seenSet = new Set(state.seenSignatures);
|
|
90
|
+
return tasks.filter(task => {
|
|
91
|
+
const signature = AgentDiary.normalizeSignature(task.title);
|
|
92
|
+
return !seenSet.has(signature);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Updates a claimed task with its final result.
|
|
97
|
+
*/
|
|
98
|
+
async writeTaskResult(title, result) {
|
|
99
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
100
|
+
await this.storage.withLock(`diary_${this.agentId}`, async () => {
|
|
101
|
+
const state = await this.readDiary();
|
|
102
|
+
const recordIndex = state.history.findIndex(r => r.signature === signature);
|
|
103
|
+
if (recordIndex !== -1) {
|
|
104
|
+
state.history[recordIndex].result = result;
|
|
105
|
+
state.history[recordIndex].timestamp = Date.now(); // update timestamp
|
|
106
|
+
state.lastRun = Date.now();
|
|
107
|
+
await this.storage.set(`diary_${this.agentId}`, state);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// If not claimed first, we throw a loud error
|
|
111
|
+
throw new Error(`[AgentDiary] Task "${title}" was not claimed. Call claimTask() before writeTaskResult().`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.AgentDiary = AgentDiary;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./storage"), exports);
|
|
18
|
+
__exportStar(require("./diary"), exports);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StorageAdapter<T> {
|
|
2
|
+
get(key: string): Promise<T | null>;
|
|
3
|
+
set(key: string, value: T): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Acquires a lock on the key, executes the critical section, and releases the lock.
|
|
6
|
+
*/
|
|
7
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
8
|
+
}
|
|
9
|
+
export declare class LocalFileStorage<T> implements StorageAdapter<T> {
|
|
10
|
+
private baseDir;
|
|
11
|
+
constructor(options?: {
|
|
12
|
+
baseDir?: string;
|
|
13
|
+
});
|
|
14
|
+
private getFilePath;
|
|
15
|
+
get(key: string): Promise<T | null>;
|
|
16
|
+
set(key: string, value: T): Promise<void>;
|
|
17
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LocalFileStorage = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const lockfile = __importStar(require("proper-lockfile"));
|
|
40
|
+
class LocalFileStorage {
|
|
41
|
+
baseDir;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.baseDir = options.baseDir || path.join(process.cwd(), '.agent-diaries');
|
|
44
|
+
if (!fs.existsSync(this.baseDir)) {
|
|
45
|
+
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
getFilePath(key) {
|
|
49
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
50
|
+
return path.join(this.baseDir, `${safeKey}.json`);
|
|
51
|
+
}
|
|
52
|
+
async get(key) {
|
|
53
|
+
const filePath = this.getFilePath(key);
|
|
54
|
+
if (!fs.existsSync(filePath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const data = await fs.promises.readFile(filePath, 'utf-8');
|
|
59
|
+
return JSON.parse(data);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(`[LocalFileStorage] Error reading key ${key}:`, e);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async set(key, value) {
|
|
67
|
+
const filePath = this.getFilePath(key);
|
|
68
|
+
try {
|
|
69
|
+
const data = JSON.stringify(value, null, 2);
|
|
70
|
+
await fs.promises.writeFile(filePath, data, 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.error(`[LocalFileStorage] Error writing key ${key}:`, e);
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async withLock(key, fn) {
|
|
78
|
+
const filePath = this.getFilePath(key);
|
|
79
|
+
// Ensure the file exists so we can lock it (proper-lockfile requires the file/dir to exist)
|
|
80
|
+
if (!fs.existsSync(filePath)) {
|
|
81
|
+
await fs.promises.writeFile(filePath, "null", 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
const release = await lockfile.lock(filePath, {
|
|
84
|
+
retries: { retries: 100, minTimeout: 10, maxTimeout: 100 },
|
|
85
|
+
realpath: false
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
return await fn();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await release();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.LocalFileStorage = LocalFileStorage;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-diaries/core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "The lightweight, framework-agnostic memory layer for edge AI agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -35,20 +35,18 @@
|
|
|
35
35
|
"homepage": "https://github.com/swapwarick/agent-diaries-core#readme",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/better-sqlite3": "^7.6.
|
|
39
|
-
"@types/
|
|
40
|
-
"@types/node": "^20.0.0",
|
|
38
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
39
|
+
"@types/node": "^25.9.1",
|
|
41
40
|
"@types/proper-lockfile": "^4.1.4",
|
|
42
|
-
"@vitest/coverage-v8": "^4.1.
|
|
41
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
43
42
|
"dotenv": "^17.4.2",
|
|
44
|
-
"ioredis": "^5.
|
|
45
|
-
"mongodb": "^
|
|
43
|
+
"ioredis": "^5.10.1",
|
|
44
|
+
"mongodb": "^7.2.0",
|
|
46
45
|
"ts-node": "^10.9.2",
|
|
47
|
-
"typescript": "^
|
|
48
|
-
"vitest": "^4.1.
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"vitest": "^4.1.7"
|
|
49
48
|
},
|
|
50
49
|
"dependencies": {
|
|
51
|
-
"mempalace-agent": "^1.0.0",
|
|
52
50
|
"proper-lockfile": "^4.1.2"
|
|
53
51
|
},
|
|
54
52
|
"peerDependencies": {
|
|
@@ -67,4 +65,4 @@
|
|
|
67
65
|
"optional": true
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
|
-
}
|
|
68
|
+
}
|