@pasko70/pibo 3.2.4 → 3.2.5

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.
@@ -34,11 +34,8 @@ export class PayloadStore {
34
34
  const createdAt = input.createdAt ?? new Date().toISOString();
35
35
  const bytes = payloadToBytes(input.value, contentType);
36
36
  const sha256 = createHash("sha256").update(bytes).digest("hex");
37
- const existing = this.findBySha256(sha256);
37
+ const existing = this.findByIdentity(sha256, contentType, input.retentionClass);
38
38
  if (existing) {
39
- if (existing.contentType !== contentType || existing.retentionClass !== input.retentionClass) {
40
- throw new PiboPayloadMetadataConflictError(sha256, existing.contentType, contentType, existing.retentionClass, input.retentionClass);
41
- }
42
39
  this.db.prepare("UPDATE payloads SET ref_count = ref_count + 1 WHERE id = ?").run(existing.id);
43
40
  return this.getPayload(existing.id) ?? existing;
44
41
  }
@@ -46,7 +43,7 @@ export class PayloadStore {
46
43
  const encoding = shouldCompress ? "gzip" : "identity";
47
44
  const bytesToStore = shouldCompress ? gzipSync(bytes) : bytes;
48
45
  const compressedByteSize = shouldCompress ? bytesToStore.byteLength : null;
49
- const relativePath = buildRelativePayloadPath(sha256, contentType, encoding);
46
+ const relativePath = buildRelativePayloadPath(sha256, contentType, input.retentionClass, encoding);
50
47
  const absolutePath = this.rootDir === ":memory:" ? relativePath : join(this.rootDir, relativePath);
51
48
  writePayloadFile(absolutePath, bytesToStore);
52
49
  const id = input.id ?? `payload_${randomUUID()}`;
@@ -127,7 +124,14 @@ export class PayloadStore {
127
124
  return JSON.parse(Buffer.from(this.readPayloadBytesBounded(id, maxBytes)).toString("utf8"));
128
125
  }
129
126
  findBySha256(sha256) {
130
- const row = this.db.prepare("SELECT * FROM payloads WHERE sha256 = ?").get(sha256);
127
+ const row = this.db.prepare("SELECT * FROM payloads WHERE sha256 = ? ORDER BY created_at ASC, id ASC LIMIT 1").get(sha256);
128
+ return row ? payloadFromRow(row) : undefined;
129
+ }
130
+ findByIdentity(sha256, contentType, retentionClass) {
131
+ const row = this.db.prepare(`
132
+ SELECT * FROM payloads
133
+ WHERE sha256 = ? AND content_type = ? AND retention_class = ?
134
+ `).get(sha256, contentType, retentionClass);
131
135
  return row ? payloadFromRow(row) : undefined;
132
136
  }
133
137
  releaseReferences(id, count = 1) {
@@ -185,12 +189,12 @@ function defaultContentType(value) {
185
189
  return "application/json";
186
190
  }
187
191
  function payloadToBytes(value, contentType) {
188
- if (typeof value === "string")
189
- return Buffer.from(value, "utf8");
190
192
  if (value instanceof Uint8Array)
191
193
  return value;
192
194
  if (contentType.includes("json"))
193
195
  return Buffer.from(JSON.stringify(value), "utf8");
196
+ if (typeof value === "string")
197
+ return Buffer.from(value, "utf8");
194
198
  return Buffer.from(String(value), "utf8");
195
199
  }
196
200
  function previewTextFromValue(value) {
@@ -200,10 +204,16 @@ function previewTextFromValue(value) {
200
204
  const normalized = text.replace(/\s+/g, " ").trim();
201
205
  return normalized ? normalized.slice(0, 1024) : undefined;
202
206
  }
203
- function buildRelativePayloadPath(sha256, contentType, encoding) {
207
+ function buildRelativePayloadPath(sha256, contentType, retentionClass, encoding) {
204
208
  const extension = extensionForContentType(contentType);
205
209
  const suffix = encoding === "gzip" ? `${extension}.gz` : extension;
206
- return join("sha256", sha256.slice(0, 2), sha256.slice(2, 4), `${sha256}.${suffix}`);
210
+ const metadataHash = createHash("sha256")
211
+ .update(contentType)
212
+ .update("\0")
213
+ .update(retentionClass)
214
+ .digest("hex")
215
+ .slice(0, 16);
216
+ return join("sha256", sha256.slice(0, 2), sha256.slice(2, 4), `${sha256}.${metadataHash}.${suffix}`);
207
217
  }
208
218
  function extensionForContentType(contentType) {
209
219
  if (contentType.includes("json"))
@@ -1,6 +1,27 @@
1
- export const PIBO_DATA_SCHEMA_VERSION = 7;
1
+ export const PIBO_DATA_SCHEMA_VERSION = 8;
2
2
  const NATIVE_HISTORY_FALLBACK_SCHEMA_VERSION = 5;
3
3
  const retiredScopeColumn = ["owner", "scope"].join("_");
4
+ const payloadTableDefinition = `
5
+ id TEXT PRIMARY KEY,
6
+ sha256 TEXT NOT NULL,
7
+ storage_kind TEXT NOT NULL,
8
+ storage_path TEXT,
9
+ content_type TEXT NOT NULL,
10
+ encoding TEXT NOT NULL DEFAULT 'gzip',
11
+ byte_size INTEGER NOT NULL,
12
+ compressed_byte_size INTEGER,
13
+ preview_text TEXT,
14
+ retention_class TEXT NOT NULL,
15
+ ref_count INTEGER NOT NULL DEFAULT 0,
16
+ status TEXT NOT NULL DEFAULT 'committed',
17
+ created_at TEXT NOT NULL,
18
+ last_verified_at TEXT
19
+ `;
20
+ const payloadTableColumns = [
21
+ "id", "sha256", "storage_kind", "storage_path", "content_type", "encoding",
22
+ "byte_size", "compressed_byte_size", "preview_text", "retention_class", "ref_count",
23
+ "status", "created_at", "last_verified_at",
24
+ ];
4
25
  const retiredScopeTables = [
5
26
  {
6
27
  name: "sessions",
@@ -93,6 +114,26 @@ function rebuildRetiredScopeTables(db, tablesToRebuild) {
93
114
  `);
94
115
  }
95
116
  }
117
+ function payloadTableNeedsMetadataIdentityRebuild(db) {
118
+ const table = db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'payloads'").get();
119
+ if (!table)
120
+ return false;
121
+ const indexes = db.prepare("SELECT name FROM pragma_index_list('payloads') WHERE \"unique\" = 1").all();
122
+ return indexes.some((index) => {
123
+ const columns = db.prepare("SELECT name FROM pragma_index_info(?) ORDER BY seqno").all(index.name);
124
+ return columns.length === 1 && columns[0]?.name === "sha256";
125
+ });
126
+ }
127
+ function rebuildPayloadTableForMetadataIdentity(db) {
128
+ const columns = payloadTableColumns.join(", ");
129
+ db.exec(`
130
+ DROP TABLE IF EXISTS __pibo_schema_v8_payloads;
131
+ CREATE TABLE __pibo_schema_v8_payloads (${payloadTableDefinition});
132
+ INSERT INTO __pibo_schema_v8_payloads (${columns}) SELECT ${columns} FROM payloads;
133
+ DROP TABLE payloads;
134
+ ALTER TABLE __pibo_schema_v8_payloads RENAME TO payloads;
135
+ `);
136
+ }
96
137
  export function assertSupportedPiboDataSchemaVersion(db) {
97
138
  const version = Number(db.prepare("PRAGMA user_version").get()?.user_version ?? 0);
98
139
  if (version > PIBO_DATA_SCHEMA_VERSION) {
@@ -101,6 +142,7 @@ export function assertSupportedPiboDataSchemaVersion(db) {
101
142
  return version;
102
143
  }
103
144
  export const PIBO_DATA_SCHEMA_MIGRATION_STEPS = [
145
+ "payload-metadata-identity",
104
146
  "schema",
105
147
  "runtime-bindings",
106
148
  "render-high-water",
@@ -114,20 +156,22 @@ export const PIBO_DATA_SCHEMA_MIGRATION_STEPS = [
114
156
  export function applyPiboDataSchema(db, hooks = {}) {
115
157
  const previousVersion = assertSupportedPiboDataSchemaVersion(db);
116
158
  const tablesToRebuild = retiredScopeTablesToRebuild(db);
159
+ const rebuildPayloadTable = payloadTableNeedsMetadataIdentityRebuild(db);
160
+ const requiresTableRebuild = tablesToRebuild.length > 0 || rebuildPayloadTable;
117
161
  const ownsTransaction = !db.isTransaction;
118
- if (!ownsTransaction && tablesToRebuild.length > 0) {
162
+ if (!ownsTransaction && requiresTableRebuild) {
119
163
  throw new Error("Pibo data schema migration requires an independent transaction");
120
164
  }
121
165
  const foreignKeysEnabled = ownsTransaction
122
- && tablesToRebuild.length > 0
166
+ && requiresTableRebuild
123
167
  && Number(db.prepare("PRAGMA foreign_keys").get().foreign_keys ?? 0) === 1;
124
168
  if (foreignKeysEnabled)
125
169
  db.exec("PRAGMA foreign_keys = OFF");
126
170
  try {
127
171
  if (ownsTransaction)
128
172
  db.exec("BEGIN IMMEDIATE");
129
- applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild);
130
- if (tablesToRebuild.length > 0) {
173
+ applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild, rebuildPayloadTable);
174
+ if (requiresTableRebuild) {
131
175
  const violations = db.prepare("PRAGMA foreign_key_check").all();
132
176
  if (violations.length > 0) {
133
177
  throw new Error(`Pibo data schema migration would retain ${violations.length} foreign-key violation(s)`);
@@ -146,11 +190,14 @@ export function applyPiboDataSchema(db, hooks = {}) {
146
190
  db.exec("PRAGMA foreign_keys = ON");
147
191
  }
148
192
  }
149
- function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild) {
193
+ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild, rebuildPayloadTable) {
150
194
  const existingSessionCount = db.prepare("SELECT COUNT(*) AS count FROM sqlite_schema WHERE type = 'table' AND name = 'sessions'").get();
151
195
  const hadSessionsBeforeMigration = existingSessionCount.count > 0
152
196
  && Number(db.prepare("SELECT COUNT(*) AS count FROM sessions").get().count) > 0;
153
197
  rebuildRetiredScopeTables(db, tablesToRebuild);
198
+ if (rebuildPayloadTable)
199
+ rebuildPayloadTableForMetadataIdentity(db);
200
+ hooks.afterStep?.("payload-metadata-identity");
154
201
  db.exec(`
155
202
  CREATE TABLE IF NOT EXISTS sessions (
156
203
  id TEXT PRIMARY KEY,
@@ -300,22 +347,9 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
300
347
  );
301
348
 
302
349
 
303
- CREATE TABLE IF NOT EXISTS payloads (
304
- id TEXT PRIMARY KEY,
305
- sha256 TEXT NOT NULL UNIQUE,
306
- storage_kind TEXT NOT NULL,
307
- storage_path TEXT,
308
- content_type TEXT NOT NULL,
309
- encoding TEXT NOT NULL DEFAULT 'gzip',
310
- byte_size INTEGER NOT NULL,
311
- compressed_byte_size INTEGER,
312
- preview_text TEXT,
313
- retention_class TEXT NOT NULL,
314
- ref_count INTEGER NOT NULL DEFAULT 0,
315
- status TEXT NOT NULL DEFAULT 'committed',
316
- created_at TEXT NOT NULL,
317
- last_verified_at TEXT
318
- );
350
+ CREATE TABLE IF NOT EXISTS payloads (${payloadTableDefinition});
351
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_payloads_identity
352
+ ON payloads(sha256, content_type, retention_class);
319
353
 
320
354
  CREATE TABLE IF NOT EXISTS event_log (
321
355
  stream_id INTEGER PRIMARY KEY,
@@ -180,7 +180,7 @@ async function piboResultToMcp(result, options) {
180
180
  tool: options.tool,
181
181
  toolCallId: options.toolCallId,
182
182
  contentType: "application/json",
183
- value: encoded,
183
+ value: structuredContent,
184
184
  });
185
185
  payloadRefs.push(stored.ref);
186
186
  structuredContent = undefined;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.2.4",
3
+ "version": "3.2.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.2.4",
9
+ "version": "3.2.5",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.2.4",
3
+ "version": "3.2.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",