@pasko70/pibo 1.13.0 → 1.14.0

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.
Files changed (51) hide show
  1. package/dist/apps/chat/static-assets.js +3 -1
  2. package/dist/apps/chat/web-app.js +10 -0
  3. package/dist/apps/chat-ui/assets/{dist-B_8EcoB6.js → dist-1eQpSfYj.js} +1 -1
  4. package/dist/apps/chat-ui/assets/{dist-DVPXynJE.js → dist-BG1J2hGI.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-UYBI2F8E.js → dist-BIwXuAMQ.js} +1 -1
  6. package/dist/apps/{context-files-ui/assets/dist-D6pYe6d_.js → chat-ui/assets/dist-BeCHnL7P.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-CVLKu7NV.js → dist-CtsdsIpj.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-DugRsLaG.js → dist-DfdQQObC.js} +1 -1
  9. package/dist/apps/{context-files-ui/assets/dist-BFRDpzcv.js → chat-ui/assets/dist-DmqwP_tM.js} +1 -1
  10. package/dist/apps/{context-files-ui/assets/dist-B5mpygOZ.js → chat-ui/assets/dist-LWXXqiGT.js} +1 -1
  11. package/dist/apps/{context-files-ui/assets/dist-B_zXPNMb.js → chat-ui/assets/dist-hoDORUlL.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-XmG4qG-A.js → dist-ur4yXrlj.js} +1 -1
  13. package/dist/apps/chat-ui/assets/index-CoCl8FG8.css +1 -0
  14. package/dist/apps/chat-ui/assets/index-DLNTzhdh.js +237 -0
  15. package/dist/apps/chat-ui/index.html +2 -2
  16. package/dist/apps/chat-vscode-web/assets/index-R_7kgvvD.js +41 -0
  17. package/dist/apps/chat-vscode-web/index.html +1 -1
  18. package/dist/apps/{chat-ui/assets/dist-CnX234jg.js → context-files-ui/assets/dist-B7gXtTZC.js} +1 -1
  19. package/dist/apps/{chat-ui/assets/dist-CELRUR8b.js → context-files-ui/assets/dist-BD262Ouw.js} +1 -1
  20. package/dist/apps/context-files-ui/assets/{dist-Em3LWmNi.js → dist-BKuGdcnX.js} +1 -1
  21. package/dist/apps/{chat-ui/assets/dist-CVmpu9FK.js → context-files-ui/assets/dist-C-Jm45P4.js} +1 -1
  22. package/dist/apps/context-files-ui/assets/{dist-DM6yvMZv.js → dist-CHdx1Jri.js} +1 -1
  23. package/dist/apps/context-files-ui/assets/{dist-BLI6sQZG.js → dist-CULMH-fB.js} +1 -1
  24. package/dist/apps/context-files-ui/assets/{dist-CLrlJjPP.js → dist-Cjq6RWEt.js} +1 -1
  25. package/dist/apps/context-files-ui/assets/{dist-D8ppb7gk.js → dist-CntqFySz.js} +1 -1
  26. package/dist/apps/context-files-ui/assets/{dist-SEP88Pg7.js → dist-DxEGebCR.js} +1 -1
  27. package/dist/apps/{chat-ui/assets/dist-KOXty1JB.js → context-files-ui/assets/dist-kPuQud7t.js} +1 -1
  28. package/dist/apps/context-files-ui/assets/index-DZWsbuvg.js +202 -0
  29. package/dist/apps/context-files-ui/index.html +1 -1
  30. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  31. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.14.0.vsix +0 -0
  32. package/dist/cli-session/localSessionSource.js +2 -0
  33. package/dist/core/session-router.js +3 -0
  34. package/dist/gateway/client.js +8 -0
  35. package/dist/gateway/server.js +18 -2
  36. package/dist/gateway/web.js +1 -0
  37. package/dist/local/extension.js +2 -0
  38. package/dist/loops/plugin.js +34 -1
  39. package/dist/loops/service.js +85 -0
  40. package/dist/mcp/config-command.js +5 -3
  41. package/dist/plugins/context-files-store.js +287 -101
  42. package/dist/plugins/context-files.js +102 -154
  43. package/dist/shared/trace-live-reducer.js +6 -1
  44. package/dist/web/channel.js +105 -16
  45. package/dist/web/http.js +14 -2
  46. package/package.json +11 -7
  47. package/dist/apps/chat-ui/assets/index-DhFQ1th-.js +0 -237
  48. package/dist/apps/chat-ui/assets/index-hjygk9Ol.css +0 -1
  49. package/dist/apps/chat-vscode-web/assets/index-1wWonouX.js +0 -41
  50. package/dist/apps/context-files-ui/assets/index-C7X2UvWe.js +0 -202
  51. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.13.0.vsix +0 -0
@@ -1,8 +1,9 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { dirname, extname, isAbsolute, join, resolve } from "node:path";
4
+ import { dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { readFallbackPidFile, readPidFile } from "../gateway/pidfile.js";
6
7
  import { PiboWebHttpError, readJsonBody, responseHtml, responseJson } from "../web/http.js";
7
8
  import { definePiboPlugin } from "./registry.js";
8
9
  import { buildContextFileDiff, ContextFileMetadataStore, hashContextFileContent, } from "./context-files-store.js";
@@ -33,6 +34,18 @@ function resolveContextFilesPaths(options) {
33
34
  agentWorkspaceRoot: resolve(options.agentWorkspaceRoot ?? join(getPiboHome(), "agent-workspaces")),
34
35
  };
35
36
  }
37
+ function assertContextFilesStorageOwnership(paths) {
38
+ const piboHome = resolve(getPiboHome());
39
+ const metadataPath = resolve(paths.metadataPath);
40
+ if (metadataPath !== piboHome && !metadataPath.startsWith(`${piboHome}${sep}`))
41
+ return;
42
+ const ownerPid = [readPidFile(), readFallbackPidFile()]
43
+ .find((pid) => pid !== undefined && pid !== process.pid);
44
+ if (ownerPid === undefined)
45
+ return;
46
+ throw new Error(`Context Files storage is owned by the active gateway process ${ownerPid}. ` +
47
+ "Use an isolated PIBO_HOME for candidate validation, or stop and activate the gateway package before migrating this storage.");
48
+ }
36
49
  function normalizeLabel(value) {
37
50
  if (typeof value !== "string")
38
51
  throw new PiboWebHttpError("label must be a string", 400);
@@ -96,6 +109,16 @@ function normalizeRevisionId(value) {
96
109
  throw new PiboWebHttpError("revisionId is required", 400);
97
110
  return value.trim();
98
111
  }
112
+ function normalizeRevisionName(value) {
113
+ if (typeof value !== "string")
114
+ throw new PiboWebHttpError("name must be a string", 400);
115
+ const trimmed = value.trim();
116
+ if (!trimmed)
117
+ throw new PiboWebHttpError("name is required", 400);
118
+ if (trimmed.length > 120)
119
+ throw new PiboWebHttpError("name must be at most 120 characters", 400);
120
+ return trimmed;
121
+ }
99
122
  function normalizeDiffSide(value, fallback) {
100
123
  if (!value)
101
124
  return fallback;
@@ -196,8 +219,7 @@ function sameSnapshot(left, right) {
196
219
  && left?.updatedAt === right.updatedAt
197
220
  && left?.bytes === right.bytes
198
221
  && left?.linkState === right.linkState
199
- && left?.sourceHash === right.sourceHash
200
- && left?.activeRevisionId === right.activeRevisionId;
222
+ && left?.sourceHash === right.sourceHash;
201
223
  }
202
224
  function contentType(pathname) {
203
225
  const ext = extname(pathname);
@@ -272,7 +294,6 @@ function contextFilePayload(file) {
272
294
  ...(file.agentProfileName ? { agentProfileName: file.agentProfileName } : {}),
273
295
  ...(file.sourceRef ? { sourceRef: file.sourceRef } : {}),
274
296
  ...(file.sourceHash ? { sourceHash: file.sourceHash } : {}),
275
- ...(file.activeRevisionId ? { activeRevisionId: file.activeRevisionId } : {}),
276
297
  exists: file.exists,
277
298
  ...(file.bytes !== undefined ? { bytes: file.bytes } : {}),
278
299
  ...(file.updatedAt ? { updatedAt: file.updatedAt } : {}),
@@ -317,6 +338,7 @@ class ContextFileService {
317
338
  managed = new Map();
318
339
  snapshots = new Map();
319
340
  pollTimer;
341
+ pollFailureReported = false;
320
342
  constructor(paths, api) {
321
343
  this.paths = paths;
322
344
  this.api = api;
@@ -335,17 +357,16 @@ class ContextFileService {
335
357
  this.discoverGlobalManagedFiles(context, false);
336
358
  const managed = this.managed.get(key);
337
359
  if (managed) {
338
- const file = this.resolveManagedInfo(context, managed);
339
- const activeRevision = file.activeRevisionId ? this.store.getRevision(file.activeRevisionId) : undefined;
360
+ const record = this.ensureManagedRecordSynced(managed);
361
+ const file = this.resolveManagedInfo(context, record);
340
362
  const snapshot = await fileSnapshot(file.absolutePath);
341
- const markdown = snapshot.exists ? (snapshot.content ?? "") : (activeRevision?.content ?? "");
342
363
  return {
343
364
  ...file,
344
365
  bytes: snapshot.bytes ?? file.bytes,
345
366
  updatedAt: snapshot.updatedAt ?? file.updatedAt,
346
367
  version: snapshot.version ?? file.version,
347
368
  exists: snapshot.exists,
348
- markdown,
369
+ markdown: snapshot.exists ? (snapshot.content ?? "") : record.workingContent,
349
370
  };
350
371
  }
351
372
  const catalogFile = this.requirePluginCatalogFile(context, key);
@@ -375,23 +396,11 @@ class ContextFileService {
375
396
  managedPath: absolutePath,
376
397
  scope,
377
398
  agentProfileName,
399
+ workingContent: markdown,
378
400
  });
379
- const revision = this.store.appendRevision({
380
- contextFileKey: record.key,
381
- kind: "working",
382
- contentHash: hashContextFileContent(markdown),
383
- content: markdown,
384
- actorId: webSession.authSession.identity.userId,
385
- note: "Managed context file created",
386
- });
387
- const updated = this.store.updateFile({
388
- ...record,
389
- activeRevisionId: revision.id,
390
- updatedAt: new Date().toISOString(),
391
- });
392
- this.upsertManaged(updated);
393
- const document = await this.read(context, updated.key);
394
- this.snapshots.set(updated.key, this.snapshotFromInfo(document));
401
+ this.upsertManaged(record);
402
+ const document = await this.read(context, record.key);
403
+ this.snapshots.set(record.key, this.snapshotFromInfo(document));
395
404
  this.emitChanged(context, "context-file.created", "web", webSession.authSession.identity.userId, document);
396
405
  return document;
397
406
  }
@@ -404,45 +413,23 @@ class ContextFileService {
404
413
  const key = uniqueKey(`ctx:${slugSegment(label)}`, new Set(this.list(context).map((file) => file.key)));
405
414
  const targetDir = this.resolveManagedDir(scope, agentProfileName);
406
415
  const absolutePath = uniquePath(targetDir, managedFileName(label));
416
+ const sourceContent = sourceDescriptor.content ?? "";
407
417
  await mkdir(dirname(absolutePath), { recursive: true });
408
- await writeFile(absolutePath, sourceDescriptor.content ?? "", "utf8");
418
+ await writeFile(absolutePath, sourceContent, "utf8");
409
419
  const record = this.store.createFile({
410
420
  key,
411
421
  label,
412
422
  managedPath: absolutePath,
413
423
  scope,
414
424
  agentProfileName,
425
+ workingContent: sourceContent,
415
426
  sourceRef: this.pluginSourceRef(sourceFile),
416
427
  sourceHash: sourceDescriptor.contentHash,
428
+ sourceContent,
417
429
  });
418
- const sourceRevision = this.store.appendRevision({
419
- contextFileKey: record.key,
420
- kind: "source-snapshot",
421
- contentHash: sourceDescriptor.contentHash,
422
- content: sourceDescriptor.content ?? "",
423
- actorId: webSession.authSession.identity.userId,
424
- sourceHashAtCreation: sourceDescriptor.contentHash,
425
- note: "Plugin source linked",
426
- });
427
- const workingRevision = this.store.appendRevision({
428
- contextFileKey: record.key,
429
- kind: "working",
430
- contentHash: sourceDescriptor.contentHash,
431
- content: sourceDescriptor.content ?? "",
432
- actorId: webSession.authSession.identity.userId,
433
- basedOnRevisionId: sourceRevision.id,
434
- sourceHashAtCreation: sourceDescriptor.contentHash,
435
- note: "Managed working copy created from plugin source",
436
- });
437
- const updated = this.store.updateFile({
438
- ...record,
439
- activeRevisionId: workingRevision.id,
440
- sourceHash: sourceDescriptor.contentHash,
441
- updatedAt: new Date().toISOString(),
442
- });
443
- this.upsertManaged(updated);
444
- const document = await this.read(context, updated.key);
445
- this.snapshots.set(updated.key, this.snapshotFromInfo(document));
430
+ this.upsertManaged(record);
431
+ const document = await this.read(context, record.key);
432
+ this.snapshots.set(record.key, this.snapshotFromInfo(document));
446
433
  this.emitChanged(context, "context-file.linked_from_plugin", "web", webSession.authSession.identity.userId, document);
447
434
  return document;
448
435
  }
@@ -456,10 +443,7 @@ class ContextFileService {
456
443
  }
457
444
  if (current.markdown === markdown)
458
445
  return current;
459
- const updatedRecord = await this.writeWorkingContent(record, markdown, {
460
- actorId: webSession.authSession.identity.userId,
461
- note: "Managed context file updated",
462
- });
446
+ const updatedRecord = await this.writeWorkingContent(record, markdown);
463
447
  const updated = await this.read(context, updatedRecord.key);
464
448
  this.snapshots.set(updated.key, this.snapshotFromInfo(updated));
465
449
  this.emitChanged(context, "context-file.updated", "web", webSession.authSession.identity.userId, updated);
@@ -486,6 +470,7 @@ class ContextFileService {
486
470
  managedPath: nextPath,
487
471
  scope,
488
472
  agentProfileName,
473
+ workingContent: this.currentWorkingContent(record),
489
474
  updatedAt: new Date().toISOString(),
490
475
  });
491
476
  this.upsertManaged(updated);
@@ -499,11 +484,10 @@ class ContextFileService {
499
484
  if (!record.sourceRef)
500
485
  throw new PiboWebHttpError("Only linked managed files can be reset to source", 400);
501
486
  const liveSource = this.requireLiveSourceForRecord(context, record);
502
- this.ensureSourceSnapshot(record, liveSource, webSession.authSession.identity.userId, "Source snapshot refreshed during reset");
503
- const updatedRecord = await this.writeWorkingContent(record, liveSource.content ?? "", {
504
- actorId: webSession.authSession.identity.userId,
487
+ const sourceContent = liveSource.content ?? "";
488
+ const updatedRecord = await this.writeWorkingContent(record, sourceContent, {
505
489
  sourceHash: liveSource.contentHash,
506
- note: "Working copy reset to source",
490
+ sourceContent,
507
491
  });
508
492
  const document = await this.read(context, updatedRecord.key);
509
493
  this.snapshots.set(updatedRecord.key, this.snapshotFromInfo(document));
@@ -515,39 +499,47 @@ class ContextFileService {
515
499
  if (!record.sourceRef)
516
500
  throw new PiboWebHttpError("Only linked managed files can adopt a source", 400);
517
501
  const liveSource = this.requireLiveSourceForRecord(context, record);
518
- this.ensureSourceSnapshot(record, liveSource, webSession.authSession.identity.userId, "Source snapshot adopted");
519
- const updatedRecord = await this.writeWorkingContent(record, liveSource.content ?? "", {
520
- actorId: webSession.authSession.identity.userId,
502
+ const sourceContent = liveSource.content ?? "";
503
+ const updatedRecord = await this.writeWorkingContent(record, sourceContent, {
521
504
  sourceHash: liveSource.contentHash,
522
- note: "Plugin source adopted",
505
+ sourceContent,
523
506
  });
524
507
  const document = await this.read(context, updatedRecord.key);
525
508
  this.snapshots.set(updatedRecord.key, this.snapshotFromInfo(document));
526
509
  this.emitChanged(context, "context-file.source_adopted", "web", webSession.authSession.identity.userId, document);
527
510
  return document;
528
511
  }
512
+ async createRevision(context, key, body, webSession) {
513
+ const record = this.ensureManagedRecordSynced(this.requireManagedRecord(key));
514
+ const name = normalizeRevisionName(body.name);
515
+ const content = this.currentWorkingContent(record);
516
+ const revision = this.store.appendRevision({
517
+ contextFileKey: key,
518
+ name,
519
+ contentHash: hashContextFileContent(content),
520
+ content,
521
+ actorId: webSession.authSession.identity.userId,
522
+ });
523
+ const document = await this.read(context, key);
524
+ this.emitChanged(context, "context-file.revision_created", "web", webSession.authSession.identity.userId, document);
525
+ return this.revisionInfoFromRecord(revision);
526
+ }
529
527
  async restoreRevision(context, key, body, webSession) {
530
528
  const record = this.requireManagedRecord(key);
531
529
  const revisionId = normalizeRevisionId(body.revisionId);
532
530
  const revision = this.store.getRevision(revisionId);
533
531
  if (!revision || revision.contextFileKey !== key)
534
532
  throw new PiboWebHttpError(`Unknown revision "${revisionId}"`, 404);
535
- const updatedRecord = await this.writeWorkingContent(record, revision.content, {
536
- actorId: webSession.authSession.identity.userId,
537
- basedOnRevisionId: revision.id,
538
- note: `Revision restored from ${revision.id}`,
539
- });
533
+ const updatedRecord = await this.writeWorkingContent(record, revision.content);
540
534
  const document = await this.read(context, updatedRecord.key);
541
535
  this.snapshots.set(updatedRecord.key, this.snapshotFromInfo(document));
542
536
  this.emitChanged(context, "context-file.revision_restored", "web", webSession.authSession.identity.userId, document);
543
537
  return document;
544
538
  }
545
539
  listRevisions(context, key) {
546
- const record = this.ensureManagedRecordSynced(this.requireManagedRecord(key));
547
- const revisions = this.store.listRevisions(key).map((revision) => this.revisionInfoFromRecord(revision, record.activeRevisionId));
540
+ this.ensureManagedRecordSynced(this.requireManagedRecord(key));
548
541
  return {
549
- revisions,
550
- ...(record.activeRevisionId ? { activeRevisionId: record.activeRevisionId } : {}),
542
+ revisions: this.store.listRevisions(key).map((revision) => this.revisionInfoFromRecord(revision)),
551
543
  };
552
544
  }
553
545
  diff(context, key, baseKind, targetKind) {
@@ -586,7 +578,14 @@ class ContextFileService {
586
578
  for (const file of this.list(context))
587
579
  this.snapshots.set(file.key, this.snapshotFromInfo(file));
588
580
  this.pollTimer = setInterval(() => {
589
- void this.poll(context);
581
+ void this.poll(context).then(() => {
582
+ this.pollFailureReported = false;
583
+ }).catch((error) => {
584
+ if (this.pollFailureReported)
585
+ return;
586
+ this.pollFailureReported = true;
587
+ console.error(`[pibo] Context Files polling failed: ${error instanceof Error ? error.message : String(error)}`);
588
+ });
590
589
  }, POLL_INTERVAL_MS);
591
590
  this.pollTimer.unref?.();
592
591
  }
@@ -645,30 +644,17 @@ class ContextFileService {
645
644
  label,
646
645
  managedPath: absolutePath,
647
646
  scope: "global",
647
+ workingContent: snapshot.content ?? "",
648
648
  createdAt: snapshot.updatedAt,
649
649
  updatedAt: snapshot.updatedAt,
650
650
  });
651
- const revision = this.store.appendRevision({
652
- contextFileKey: record.key,
653
- kind: "working",
654
- contentHash: snapshot.version,
655
- content: snapshot.content ?? "",
656
- createdAt: snapshot.updatedAt,
657
- note: "Global context file discovered on disk",
658
- });
659
- const updated = this.store.updateFile({
660
- ...record,
661
- activeRevisionId: revision.id,
662
- updatedAt: snapshot.updatedAt ?? new Date().toISOString(),
663
- });
664
- this.upsertManaged(updated);
665
- this.managed.set(updated.key, updated);
651
+ this.upsertManaged(record);
666
652
  knownPaths.add(absolutePath);
667
- usedKeys.add(updated.key);
668
- const info = this.resolveManagedInfo(context, updated);
669
- discovered.push(updated);
653
+ usedKeys.add(record.key);
654
+ const info = this.resolveManagedInfo(context, record);
655
+ discovered.push(record);
670
656
  if (emitEvents) {
671
- this.snapshots.set(updated.key, this.snapshotFromInfo(info));
657
+ this.snapshots.set(record.key, this.snapshotFromInfo(info));
672
658
  this.emitChanged(context, "context-file.created", "filesystem", undefined, info);
673
659
  }
674
660
  }
@@ -707,10 +693,9 @@ class ContextFileService {
707
693
  resolveManagedInfo(context, file) {
708
694
  const record = this.ensureManagedRecordSynced(file);
709
695
  const snapshot = snapshotFromSync(record.managedPath);
710
- const activeRevision = record.activeRevisionId ? this.store.getRevision(record.activeRevisionId) : undefined;
711
696
  const sourceDescriptor = record.sourceRef ? this.resolveLiveSource(context, record.sourceRef) : undefined;
712
697
  const sourceHash = sourceDescriptor?.contentHash ?? record.sourceHash;
713
- const workingHash = snapshot.version ?? activeRevision?.contentHash;
698
+ const workingHash = snapshot.version ?? hashContextFileContent(record.workingContent);
714
699
  const linkState = this.computeLinkState(record, sourceDescriptor, workingHash);
715
700
  return {
716
701
  key: record.key,
@@ -725,13 +710,12 @@ class ContextFileService {
725
710
  editable: true,
726
711
  removable: true,
727
712
  exists: snapshot.exists,
728
- bytes: snapshot.bytes ?? (activeRevision ? Buffer.byteLength(activeRevision.content, "utf8") : undefined),
713
+ bytes: snapshot.bytes ?? Buffer.byteLength(record.workingContent, "utf8"),
729
714
  updatedAt: snapshot.updatedAt ?? record.updatedAt,
730
- version: snapshot.version ?? activeRevision?.contentHash,
715
+ version: snapshot.version ?? workingHash,
731
716
  sourceRef: record.sourceRef,
732
717
  sourceHash,
733
718
  linkState,
734
- activeRevisionId: record.activeRevisionId,
735
719
  };
736
720
  }
737
721
  computeLinkState(record, sourceDescriptor, workingHash) {
@@ -747,21 +731,12 @@ class ContextFileService {
747
731
  const snapshot = snapshotFromSync(record.managedPath);
748
732
  if (!snapshot.exists || !snapshot.version)
749
733
  return record;
750
- const activeRevision = record.activeRevisionId ? this.store.getRevision(record.activeRevisionId) : undefined;
751
- if (activeRevision?.contentHash === snapshot.version)
734
+ const content = snapshot.content ?? "";
735
+ if (hashContextFileContent(record.workingContent) === snapshot.version)
752
736
  return record;
753
- const revision = this.store.appendRevision({
754
- contextFileKey: record.key,
755
- kind: "working",
756
- contentHash: snapshot.version,
757
- content: snapshot.content ?? "",
758
- basedOnRevisionId: record.activeRevisionId,
759
- sourceHashAtCreation: record.sourceHash,
760
- note: activeRevision ? "External file update detected" : "Recovered working content from disk",
761
- });
762
737
  const updated = this.store.updateFile({
763
738
  ...record,
764
- activeRevisionId: revision.id,
739
+ workingContent: content,
765
740
  updatedAt: snapshot.updatedAt ?? new Date().toISOString(),
766
741
  });
767
742
  this.upsertManaged(updated);
@@ -769,59 +744,29 @@ class ContextFileService {
769
744
  }
770
745
  currentWorkingContent(record) {
771
746
  const snapshot = snapshotFromSync(record.managedPath);
772
- if (snapshot.exists)
773
- return snapshot.content ?? "";
774
- const revision = record.activeRevisionId ? this.store.getRevision(record.activeRevisionId) : undefined;
775
- return revision?.content ?? "";
747
+ return snapshot.exists ? (snapshot.content ?? "") : record.workingContent;
776
748
  }
777
- async writeWorkingContent(record, content, options) {
749
+ async writeWorkingContent(record, content, options = {}) {
778
750
  await mkdir(dirname(record.managedPath), { recursive: true });
779
751
  await writeFile(record.managedPath, content, "utf8");
780
- const revision = this.store.appendRevision({
781
- contextFileKey: record.key,
782
- kind: "working",
783
- contentHash: hashContextFileContent(content),
784
- content,
785
- actorId: options.actorId,
786
- basedOnRevisionId: options.basedOnRevisionId ?? record.activeRevisionId,
787
- sourceHashAtCreation: options.sourceHash ?? record.sourceHash,
788
- note: options.note,
789
- });
790
752
  const updated = this.store.updateFile({
791
753
  ...record,
792
- activeRevisionId: revision.id,
754
+ workingContent: content,
793
755
  sourceHash: options.sourceHash ?? record.sourceHash,
756
+ sourceContent: options.sourceContent ?? record.sourceContent,
794
757
  updatedAt: new Date().toISOString(),
795
758
  });
796
759
  this.upsertManaged(updated);
797
760
  return updated;
798
761
  }
799
- ensureSourceSnapshot(record, source, actorId, note) {
800
- const existing = source.contentHash ? this.store.findLatestSourceSnapshot(record.key, source.contentHash) : undefined;
801
- if (existing)
802
- return existing;
803
- return this.store.appendRevision({
804
- contextFileKey: record.key,
805
- kind: "source-snapshot",
806
- contentHash: source.contentHash ?? hashContextFileContent(source.content ?? ""),
807
- content: source.content ?? "",
808
- actorId,
809
- sourceHashAtCreation: source.contentHash,
810
- note,
811
- });
812
- }
813
- revisionInfoFromRecord(revision, activeRevisionId) {
762
+ revisionInfoFromRecord(revision) {
814
763
  return {
815
764
  id: revision.id,
816
- kind: revision.kind,
765
+ name: revision.name,
817
766
  contentHash: revision.contentHash,
818
767
  createdAt: revision.createdAt,
819
768
  actorId: revision.actorId,
820
- basedOnRevisionId: revision.basedOnRevisionId,
821
- sourceHashAtCreation: revision.sourceHashAtCreation,
822
- note: revision.note,
823
769
  content: revision.content,
824
- active: revision.id === activeRevisionId,
825
770
  };
826
771
  }
827
772
  resolveDiffSide(context, record, kind) {
@@ -836,14 +781,11 @@ class ContextFileService {
836
781
  contentHash: liveSource.contentHash,
837
782
  };
838
783
  }
839
- const snapshot = record.sourceHash
840
- ? this.store.findLatestSourceSnapshot(record.key, record.sourceHash)
841
- : this.store.findLatestSourceSnapshot(record.key);
842
- if (!snapshot)
784
+ if (record.sourceContent === undefined)
843
785
  throw new PiboWebHttpError("No source snapshot available for diff", 404);
844
786
  return {
845
- content: snapshot.content,
846
- contentHash: snapshot.contentHash,
787
+ content: record.sourceContent,
788
+ contentHash: record.sourceHash ?? hashContextFileContent(record.sourceContent),
847
789
  };
848
790
  }
849
791
  requirePluginCatalogFile(context, key) {
@@ -924,7 +866,6 @@ class ContextFileService {
924
866
  version: file.version,
925
867
  linkState: file.linkState,
926
868
  sourceHash: file.sourceHash,
927
- activeRevisionId: file.activeRevisionId,
928
869
  };
929
870
  }
930
871
  emitChanged(context, type, source, actorId, file) {
@@ -1054,6 +995,12 @@ function createContextFilesWebApp(service) {
1054
995
  service.startWatcher(context);
1055
996
  return responseJson(service.listRevisions(context, path.key));
1056
997
  }
998
+ if (path.action === "revisions" && request.method === "POST") {
999
+ const webSession = await context.requireSession({ request });
1000
+ service.startWatcher(context);
1001
+ const body = await readJsonBody(request);
1002
+ return responseJson({ revision: await service.createRevision(context, path.key, body, webSession) }, { status: 201 });
1003
+ }
1057
1004
  if (path.action === "diff" && request.method === "GET") {
1058
1005
  await context.requireSession({ request });
1059
1006
  service.startWatcher(context);
@@ -1076,6 +1023,7 @@ export function createPiboContextFilesPlugin(options = {}) {
1076
1023
  id: "pibo.context-files",
1077
1024
  name: "Pibo Context Files",
1078
1025
  register(api) {
1026
+ assertContextFilesStorageOwnership(paths);
1079
1027
  const service = new ContextFileService(paths, api);
1080
1028
  api.registerWebApp(createContextFilesWebApp(service));
1081
1029
  },
@@ -150,10 +150,15 @@ function makeStored(streamEvent, piboSessionId, type, payload, nextSequence, now
150
150
  streamFrameIndex,
151
151
  eventId: typeof payload.eventId === "string" ? payload.eventId : undefined,
152
152
  type,
153
- createdAt: now(),
153
+ createdAt: streamEventCreatedAt(streamEvent) ?? now(),
154
154
  payload,
155
155
  };
156
156
  }
157
+ function streamEventCreatedAt(event) {
158
+ if (typeof event.createdAt !== "string" || !Number.isFinite(Date.parse(event.createdAt)))
159
+ return undefined;
160
+ return event.createdAt;
161
+ }
157
162
  function dropMatching(events, finalEvent, dropType) {
158
163
  const finalKey = eventGroupKey(finalEvent);
159
164
  return events.filter((event) => event.type !== dropType || eventGroupKey(event) !== finalKey);