@pellux/goodvibes-sdk 0.37.0 → 0.37.1

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.
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.37.0"
6
+ "version": "0.37.1"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- let version = '0.37.0';
3
+ let version = '0.37.1';
4
4
  try {
5
5
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf-8'));
6
6
  version = pkg.version ?? version;
@@ -75,6 +75,30 @@ export declare class WorkspaceCheckpointManager {
75
75
  private checkpoints;
76
76
  private initialized;
77
77
  private initPromise;
78
+ /**
79
+ * Promise-chain mutex serializing every public operation that touches the
80
+ * side repo's shared index or shared object store: `create`, `restore`,
81
+ * `gc`, and `diff` (see each method's `*Internal` body below). Without
82
+ * this, an auto-snapshot `create()` firing on a bus event (TURN_COMPLETED,
83
+ * TURN_ERROR, TURN_CANCEL, or AGENT_COMPLETED) could run its `git add -A`
84
+ * in between `restore()`'s
85
+ * `read-tree --reset` and `checkout-index -a -f`, silently corrupting the
86
+ * restore; two concurrent `create()` calls share the same hazard on the
87
+ * index, and a same-tick `gc()` could treat a not-yet-ref'd loose commit
88
+ * from an in-flight `create()` as unreachable and prune it out from under
89
+ * it. `diff()` is included too: the single-argument `git diff <tree-ish>`
90
+ * form (diffing a checkpoint against the live working tree) refreshes the
91
+ * index's stat cache as a side effect, which is itself a write.
92
+ *
93
+ * Each public method below only does `await this.init()` (idempotent, safe
94
+ * to race) before calling `withLock`; the actual git-touching work lives in
95
+ * a same-named `*Internal` method. Internal callers that need another
96
+ * locked operation's behavior (e.g. `restore()`'s safety checkpoint) call
97
+ * the `*Internal` method directly — never the public wrapper — so a single
98
+ * logical operation never tries to re-enter its own lock.
99
+ */
100
+ private lockChain;
101
+ private withLock;
78
102
  constructor(opts: WorkspaceCheckpointManagerOptions);
79
103
  /**
80
104
  * Idempotent setup: init the side repo, load the manifest (re-hydrating the
@@ -103,15 +127,25 @@ export declare class WorkspaceCheckpointManager {
103
127
  * Create a new checkpoint. Returns `null` (a cheap no-op) when the current
104
128
  * workspace tree is identical to the parent checkpoint's tree — no commit,
105
129
  * no ref, no manifest entry is created in that case.
130
+ *
131
+ * Serialized against every other index-touching operation on this manager
132
+ * — see `withLock`.
106
133
  */
107
134
  create(opts: CreateCheckpointOptions): Promise<WorkspaceCheckpoint | null>;
135
+ private createInternal;
108
136
  /** List checkpoints, newest-first. `list()[0]` is always the most recent checkpoint. */
109
137
  list(filter?: ListCheckpointsFilter): Promise<WorkspaceCheckpoint[]>;
110
138
  /**
111
139
  * Diff two checkpoints, or a checkpoint against the live working tree when
112
140
  * `b` is omitted.
141
+ *
142
+ * Serialized against every other index-touching operation on this manager
143
+ * — see `withLock`. The single-argument form (`b` omitted, diffing against
144
+ * the live working tree) refreshes the side index's stat cache as a side
145
+ * effect, so it is not purely read-only.
113
146
  */
114
147
  diff(a: string, b?: string): Promise<CheckpointDiff>;
148
+ private diffInternal;
115
149
  /**
116
150
  * Restore the workspace to the state captured by checkpoint `id`.
117
151
  *
@@ -133,8 +167,14 @@ export declare class WorkspaceCheckpointManager {
133
167
  *
134
168
  * Scoped restore (`opts.paths` provided) only checks out those paths from
135
169
  * the target tree; it never removes files outside the given paths.
170
+ *
171
+ * Serialized against every other index-touching operation on this manager
172
+ * — see `withLock`. Without this, an auto-snapshot `create()` firing on a
173
+ * bus event could run its `git add -A` in between the `read-tree --reset`
174
+ * and `checkout-index -a -f` calls below, silently corrupting the restore.
136
175
  */
137
176
  restore(id: string, opts?: RestoreOptions): Promise<RestoreResult>;
177
+ private restoreInternal;
138
178
  /**
139
179
  * Apply retention limits: `RetentionPolicy` selects prune candidates,
140
180
  * `WorkspaceCheckpointPruner` deletes their refs, then (only if anything
@@ -142,8 +182,19 @@ export declare class WorkspaceCheckpointManager {
142
182
  * unreachable objects. This never touches compaction's boundary commits —
143
183
  * they are tracked in an entirely separate `RetentionPolicy` instance
144
184
  * (../../runtime/compaction) with no shared state.
185
+ *
186
+ * Reclamation only works because checkpoint commits are parentless (see
187
+ * `SideGitRunner.commitTree`): a pruned ref's commit has no descendant
188
+ * keeping it reachable via a git parent pointer, so once its ref is
189
+ * deleted it is genuinely unreachable and `--prune=now` frees it.
190
+ *
191
+ * Serialized against every other index/object-store-touching operation on
192
+ * this manager — see `withLock`. Without this, a `create()` racing this
193
+ * method could write a loose commit object that isn't ref'd yet at the
194
+ * moment `--prune=now` runs, and lose it.
145
195
  */
146
196
  gc(): Promise<PruneResult>;
197
+ private gcInternal;
147
198
  /** Unsubscribe from the runtime bus. Does not touch anything on disk. */
148
199
  dispose(): void;
149
200
  private mostRecentCheckpoint;
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../../src/platform/workspace/checkpoint/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AASH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,cAAc,EAGnB,KAAK,WAAW,EAEjB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAuFrG,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACtC,wFAAwF;IACxF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,kGAAkG;IAClG,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,8HAA8H;IAC9H,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,SAAS,CAAC;IAC1D,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CAC3C;AAED;;;;GAIG;AACH,qBAAa,0BAA0B;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA0B;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IAEpD,OAAO,CAAC,WAAW,CAA0C;IAC7D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAA8B;gBAErC,IAAI,EAAE,iCAAiC;IAiBnD;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAQb,KAAK;IAuBnB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IAuCrC;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAwDhF,wFAAwF;IAClF,IAAI,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAS1E;;;OAGG;IACG,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAoB1D;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAgExE;;;;;;;OAOG;IACG,EAAE,IAAI,OAAO,CAAC,WAAW,CAAC;IAUhC,yEAAyE;IACzE,OAAO,IAAI,IAAI;IAef,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,YAAY;IAKpB;;;;;;;OAOG;YACW,gBAAgB;YAehB,eAAe;CAG9B"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../../src/platform/workspace/checkpoint/manager.ts"],"names":[],"mappings":"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"}
@@ -130,6 +130,39 @@ export class WorkspaceCheckpointManager {
130
130
  checkpoints = new Map();
131
131
  initialized = false;
132
132
  initPromise = null;
133
+ /**
134
+ * Promise-chain mutex serializing every public operation that touches the
135
+ * side repo's shared index or shared object store: `create`, `restore`,
136
+ * `gc`, and `diff` (see each method's `*Internal` body below). Without
137
+ * this, an auto-snapshot `create()` firing on a bus event (TURN_COMPLETED,
138
+ * TURN_ERROR, TURN_CANCEL, or AGENT_COMPLETED) could run its `git add -A`
139
+ * in between `restore()`'s
140
+ * `read-tree --reset` and `checkout-index -a -f`, silently corrupting the
141
+ * restore; two concurrent `create()` calls share the same hazard on the
142
+ * index, and a same-tick `gc()` could treat a not-yet-ref'd loose commit
143
+ * from an in-flight `create()` as unreachable and prune it out from under
144
+ * it. `diff()` is included too: the single-argument `git diff <tree-ish>`
145
+ * form (diffing a checkpoint against the live working tree) refreshes the
146
+ * index's stat cache as a side effect, which is itself a write.
147
+ *
148
+ * Each public method below only does `await this.init()` (idempotent, safe
149
+ * to race) before calling `withLock`; the actual git-touching work lives in
150
+ * a same-named `*Internal` method. Internal callers that need another
151
+ * locked operation's behavior (e.g. `restore()`'s safety checkpoint) call
152
+ * the `*Internal` method directly — never the public wrapper — so a single
153
+ * logical operation never tries to re-enter its own lock.
154
+ */
155
+ lockChain = Promise.resolve();
156
+ withLock(fn) {
157
+ // `lockChain` is constructed below so it never itself rejects (its
158
+ // continuation always swallows both outcomes) — chaining with a single
159
+ // `.then(fn)` is enough to guarantee `fn` only starts after every
160
+ // previously-queued operation has settled, regardless of whether that
161
+ // prior operation resolved or rejected.
162
+ const result = this.lockChain.then(fn);
163
+ this.lockChain = result.then(() => undefined, () => undefined);
164
+ return result;
165
+ }
133
166
  constructor(opts) {
134
167
  this.workspaceRoot = opts.workspaceRoot;
135
168
  this.checkpointRootDir = opts.checkpointDir ?? join(opts.workspaceRoot, '.goodvibes', 'checkpoints');
@@ -226,9 +259,15 @@ export class WorkspaceCheckpointManager {
226
259
  * Create a new checkpoint. Returns `null` (a cheap no-op) when the current
227
260
  * workspace tree is identical to the parent checkpoint's tree — no commit,
228
261
  * no ref, no manifest entry is created in that case.
262
+ *
263
+ * Serialized against every other index-touching operation on this manager
264
+ * — see `withLock`.
229
265
  */
230
266
  async create(opts) {
231
267
  await this.init();
268
+ return this.withLock(() => this.createInternal(opts));
269
+ }
270
+ async createInternal(opts) {
232
271
  await this.sideGit.stageAll(opts.paths);
233
272
  const treeHash = await this.sideGit.writeTree();
234
273
  const parent = this.mostRecentCheckpoint();
@@ -245,7 +284,7 @@ export class WorkspaceCheckpointManager {
245
284
  const retentionClass = opts.retentionClass ?? defaultRetentionClassFor(kind);
246
285
  const label = opts.label ?? this.defaultLabel(kind, id);
247
286
  const message = `wcp: ${kind} ${label}`.trim();
248
- const commit = await this.sideGit.commitTree(treeHash, message, parent?.commit ?? null);
287
+ const commit = await this.sideGit.commitTree(treeHash, message);
249
288
  await this.sideGit.updateRef(`${CHECKPOINT_REF_PREFIX}${id}`, commit);
250
289
  const changedPaths = parent
251
290
  ? await this.sideGit.diffNameOnly(parent.commit, commit)
@@ -290,9 +329,17 @@ export class WorkspaceCheckpointManager {
290
329
  /**
291
330
  * Diff two checkpoints, or a checkpoint against the live working tree when
292
331
  * `b` is omitted.
332
+ *
333
+ * Serialized against every other index-touching operation on this manager
334
+ * — see `withLock`. The single-argument form (`b` omitted, diffing against
335
+ * the live working tree) refreshes the side index's stat cache as a side
336
+ * effect, so it is not purely read-only.
293
337
  */
294
338
  async diff(a, b) {
295
339
  await this.init();
340
+ return this.withLock(() => this.diffInternal(a, b));
341
+ }
342
+ async diffInternal(a, b) {
296
343
  const fromCheckpoint = this.requireCheckpoint(a);
297
344
  const toCheckpoint = b ? this.requireCheckpoint(b) : undefined;
298
345
  const [unifiedDiff, stat_, files] = await Promise.all([
@@ -329,15 +376,25 @@ export class WorkspaceCheckpointManager {
329
376
  *
330
377
  * Scoped restore (`opts.paths` provided) only checks out those paths from
331
378
  * the target tree; it never removes files outside the given paths.
379
+ *
380
+ * Serialized against every other index-touching operation on this manager
381
+ * — see `withLock`. Without this, an auto-snapshot `create()` firing on a
382
+ * bus event could run its `git add -A` in between the `read-tree --reset`
383
+ * and `checkout-index -a -f` calls below, silently corrupting the restore.
332
384
  */
333
385
  async restore(id, opts) {
334
386
  await this.init();
387
+ return this.withLock(() => this.restoreInternal(id, opts));
388
+ }
389
+ async restoreInternal(id, opts) {
335
390
  const target = this.requireCheckpoint(id);
336
391
  const wantSafety = opts?.safetyCheckpoint ?? true;
337
392
  let beforeFiles;
338
393
  let safetyCheckpointId = null;
339
394
  if (wantSafety) {
340
- const safety = await this.create({ kind: 'manual', label: `pre-restore safety (before ${id})`, retentionClass: 'forensic' });
395
+ // Calls createInternal directly (not the public, lock-acquiring
396
+ // `create()`) — this whole method already holds the lock.
397
+ const safety = await this.createInternal({ kind: 'manual', label: `pre-restore safety (before ${id})`, retentionClass: 'forensic' });
341
398
  safetyCheckpointId = safety?.id ?? null;
342
399
  const current = safety ?? this.mostRecentCheckpoint();
343
400
  beforeFiles = current ? await this.sideGit.listTrackedFiles(current.commit) : [];
@@ -396,9 +453,22 @@ export class WorkspaceCheckpointManager {
396
453
  * unreachable objects. This never touches compaction's boundary commits —
397
454
  * they are tracked in an entirely separate `RetentionPolicy` instance
398
455
  * (../../runtime/compaction) with no shared state.
456
+ *
457
+ * Reclamation only works because checkpoint commits are parentless (see
458
+ * `SideGitRunner.commitTree`): a pruned ref's commit has no descendant
459
+ * keeping it reachable via a git parent pointer, so once its ref is
460
+ * deleted it is genuinely unreachable and `--prune=now` frees it.
461
+ *
462
+ * Serialized against every other index/object-store-touching operation on
463
+ * this manager — see `withLock`. Without this, a `create()` racing this
464
+ * method could write a loose commit object that isn't ref'd yet at the
465
+ * moment `--prune=now` runs, and lose it.
399
466
  */
400
467
  async gc() {
401
468
  await this.init();
469
+ return this.withLock(() => this.gcInternal());
470
+ }
471
+ async gcInternal() {
402
472
  const result = await this.retentionPolicy.prune();
403
473
  if (result.deletedCount > 0) {
404
474
  await this.persistManifest();
@@ -87,11 +87,24 @@ export declare class SideGitRunner {
87
87
  /** Resolve `<commit>^{tree}` for an existing commit hash. */
88
88
  treeOf(commit: string): Promise<string>;
89
89
  /**
90
- * Create a commit object from a tree (optionally with a parent), WITHOUT
91
- * moving any branch/HEAD. The returned hash is only reachable once a ref is
92
- * pointed at it via `updateRef`.
90
+ * Create a commit object from a tree, deliberately WITHOUT a git parent
91
+ * (no `-p`) and WITHOUT moving any branch/HEAD.
92
+ *
93
+ * Checkpoint lineage is tracked exclusively via the manifest's `parentId`
94
+ * field (manager.ts) — never via git ancestry. This isn't just a style
95
+ * choice: if checkpoint commits chained via `-p` the way a normal git
96
+ * history does, deleting an OLD checkpoint's ref in `gc()` would free
97
+ * nothing, because that commit stays reachable through every NEWER
98
+ * checkpoint's parent pointer — the ref is gone but the commit (and any
99
+ * tree/blob objects unique to it) is still walkable from every surviving
100
+ * descendant. Parentless commits mean a checkpoint's own ref is the ONLY
101
+ * thing keeping its commit reachable, so once that ref is deleted,
102
+ * `git gc --prune=now` can genuinely reclaim it.
103
+ *
104
+ * The returned hash is only reachable once a ref is pointed at it via
105
+ * `updateRef`.
93
106
  */
94
- commitTree(treeHash: string, message: string, parentCommit: string | null): Promise<string>;
107
+ commitTree(treeHash: string, message: string): Promise<string>;
95
108
  updateRef(refName: string, commit: string): Promise<void>;
96
109
  deleteRef(refName: string): Promise<void>;
97
110
  /** List every ref under CHECKPOINT_REF_PREFIX as `{ id, commit }`. */
@@ -1 +1 @@
1
- {"version":3,"file":"side-git.d.ts","sourceRoot":"","sources":["../../../../src/platform/workspace/checkpoint/side-git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAQH,0EAA0E;AAC1E,eAAO,MAAM,eAAe,6CAA6C,CAAC;AAE1E,0DAA0D;AAC1D,eAAO,MAAM,qBAAqB,gCAAgC,CAAC;AA8CnE,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;gBAEpB,IAAI,EAAE,oBAAoB;IAUtC,gFAAgF;IAC1E,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1C;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;;;;;;;;OASG;IACH,OAAO,CAAC,sBAAsB;IAuB9B;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/C,wGAAwG;IAClG,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAIlC,6DAA6D;IACvD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7C;;;;OAIG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAM3F,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzD,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C,sEAAsE;IAChE,kBAAkB,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAmBrE,+DAA+D;IACzD,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK/D,+FAA+F;IACzF,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,wGAAwG;IAClG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvC,iGAAiG;IAC3F,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAItD,wGAAwG;IAClG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1D,6GAA6G;IACvG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKhE,6CAA6C;IACvC,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;CAG1B"}
1
+ {"version":3,"file":"side-git.d.ts","sourceRoot":"","sources":["../../../../src/platform/workspace/checkpoint/side-git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAQH,0EAA0E;AAC1E,eAAO,MAAM,eAAe,6CAA6C,CAAC;AAE1E,0DAA0D;AAC1D,eAAO,MAAM,qBAAqB,gCAAgC,CAAC;AA8CnE,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;gBAEpB,IAAI,EAAE,oBAAoB;IAUtC,gFAAgF;IAC1E,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1C;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;;;;;;;;OASG;IACH,OAAO,CAAC,sBAAsB;IAuB9B;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/C,wGAAwG;IAClG,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAIlC,6DAA6D;IACvD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7C;;;;;;;;;;;;;;;;;OAiBG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI9D,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzD,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C,sEAAsE;IAChE,kBAAkB,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAmBrE,+DAA+D;IACzD,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK/D,+FAA+F;IACzF,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,wGAAwG;IAClG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvC,iGAAiG;IAC3F,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAItD,wGAAwG;IAClG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1D,6GAA6G;IACvG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKhE,6CAA6C;IACvC,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;CAG1B"}
@@ -185,15 +185,25 @@ export class SideGitRunner {
185
185
  return (await this.git.raw(['rev-parse', `${commit}^{tree}`])).trim();
186
186
  }
187
187
  /**
188
- * Create a commit object from a tree (optionally with a parent), WITHOUT
189
- * moving any branch/HEAD. The returned hash is only reachable once a ref is
190
- * pointed at it via `updateRef`.
188
+ * Create a commit object from a tree, deliberately WITHOUT a git parent
189
+ * (no `-p`) and WITHOUT moving any branch/HEAD.
190
+ *
191
+ * Checkpoint lineage is tracked exclusively via the manifest's `parentId`
192
+ * field (manager.ts) — never via git ancestry. This isn't just a style
193
+ * choice: if checkpoint commits chained via `-p` the way a normal git
194
+ * history does, deleting an OLD checkpoint's ref in `gc()` would free
195
+ * nothing, because that commit stays reachable through every NEWER
196
+ * checkpoint's parent pointer — the ref is gone but the commit (and any
197
+ * tree/blob objects unique to it) is still walkable from every surviving
198
+ * descendant. Parentless commits mean a checkpoint's own ref is the ONLY
199
+ * thing keeping its commit reachable, so once that ref is deleted,
200
+ * `git gc --prune=now` can genuinely reclaim it.
201
+ *
202
+ * The returned hash is only reachable once a ref is pointed at it via
203
+ * `updateRef`.
191
204
  */
192
- async commitTree(treeHash, message, parentCommit) {
193
- const args = ['commit-tree', treeHash, '-m', message];
194
- if (parentCommit)
195
- args.push('-p', parentCommit);
196
- return (await this.git.raw(args)).trim();
205
+ async commitTree(treeHash, message) {
206
+ return (await this.git.raw(['commit-tree', treeHash, '-m', message])).trim();
197
207
  }
198
208
  async updateRef(refName, commit) {
199
209
  await this.git.raw(['update-ref', refName, commit]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-sdk",
3
- "version": "0.37.0",
3
+ "version": "0.37.1",
4
4
  "description": "TypeScript SDK for building GoodVibes operator, peer, web, mobile, and daemon-connected apps with typed contracts, auth, realtime events, and transport layers.",
5
5
  "keywords": [
6
6
  "goodvibes",
@@ -453,14 +453,14 @@
453
453
  "sideEffects": false,
454
454
  "type": "module",
455
455
  "dependencies": {
456
- "@pellux/goodvibes-contracts": "0.37.0",
457
- "@pellux/goodvibes-daemon-sdk": "0.37.0",
458
- "@pellux/goodvibes-errors": "0.37.0",
459
- "@pellux/goodvibes-operator-sdk": "0.37.0",
460
- "@pellux/goodvibes-peer-sdk": "0.37.0",
461
- "@pellux/goodvibes-transport-core": "0.37.0",
462
- "@pellux/goodvibes-transport-http": "0.37.0",
463
- "@pellux/goodvibes-transport-realtime": "0.37.0"
456
+ "@pellux/goodvibes-contracts": "0.37.1",
457
+ "@pellux/goodvibes-daemon-sdk": "0.37.1",
458
+ "@pellux/goodvibes-errors": "0.37.1",
459
+ "@pellux/goodvibes-operator-sdk": "0.37.1",
460
+ "@pellux/goodvibes-peer-sdk": "0.37.1",
461
+ "@pellux/goodvibes-transport-core": "0.37.1",
462
+ "@pellux/goodvibes-transport-http": "0.37.1",
463
+ "@pellux/goodvibes-transport-realtime": "0.37.1"
464
464
  },
465
465
  "optionalDependencies": {
466
466
  "@agentclientprotocol/sdk": "^0.21.0",