hippocamp 0.2.9 → 0.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippocamp",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "private": false,
5
5
  "description": "Local Git-backed memory for AI agents.",
6
6
  "license": "MIT",
@@ -510,12 +510,19 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
510
510
  }
511
511
 
512
512
  if (!diffResult.stdout) {
513
- return {
514
- attempted: true,
515
- skipped: true,
516
- reason: "no_changes_to_commit",
517
- repoRoot,
518
- };
513
+ // Commits left behind by an earlier failed push still need to go up.
514
+ const aheadResult = await runGit(["rev-list", "--count", "@{u}..HEAD"], repoRoot);
515
+
516
+ if (!aheadResult.ok || aheadResult.stdout === "0") {
517
+ return {
518
+ attempted: true,
519
+ skipped: true,
520
+ reason: "no_changes_to_commit",
521
+ repoRoot,
522
+ };
523
+ }
524
+
525
+ return pushToUpstream(repoRoot, { attempted: true, committed: false, repoRoot });
519
526
  }
520
527
 
521
528
  const commitResult = await runGit(["commit", "-m", message, "--", ...repoRelativePaths], repoRoot);
@@ -524,16 +531,22 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
524
531
  throw new Error(commitResult.stderr || "git commit failed.");
525
532
  }
526
533
 
534
+ return pushToUpstream(repoRoot, {
535
+ attempted: true,
536
+ committed: true,
537
+ repoRoot,
538
+ commitSummary: commitResult.stdout,
539
+ });
540
+ }
541
+
542
+ async function pushToUpstream(repoRoot, result) {
527
543
  const upstreamResult = await runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot);
528
544
 
529
545
  if (!upstreamResult.ok) {
530
546
  return {
531
- attempted: true,
532
- committed: true,
547
+ ...result,
533
548
  pushed: false,
534
549
  reason: "no_upstream",
535
- repoRoot,
536
- commitSummary: commitResult.stdout,
537
550
  };
538
551
  }
539
552
 
@@ -544,12 +557,9 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
544
557
 
545
558
  if (!pullResult.ok) {
546
559
  return {
547
- attempted: true,
548
- committed: true,
560
+ ...result,
549
561
  pushed: false,
550
562
  reason: "pull_rebase_failed",
551
- repoRoot,
552
- commitSummary: commitResult.stdout,
553
563
  pushError: pushResult.stderr,
554
564
  pullError: pullResult.stderr,
555
565
  };
@@ -559,23 +569,17 @@ async function syncMemory({ scope, projectRoot, paths, message }) {
559
569
 
560
570
  if (!pushResult.ok) {
561
571
  return {
562
- attempted: true,
563
- committed: true,
572
+ ...result,
564
573
  pushed: false,
565
574
  reason: "push_failed",
566
- repoRoot,
567
- commitSummary: commitResult.stdout,
568
575
  pushError: pushResult.stderr,
569
576
  };
570
577
  }
571
578
  }
572
579
 
573
580
  return {
574
- attempted: true,
575
- committed: true,
581
+ ...result,
576
582
  pushed: true,
577
- repoRoot,
578
- commitSummary: commitResult.stdout,
579
583
  pushSummary: pushResult.stdout || pushResult.stderr,
580
584
  };
581
585
  }
@@ -63,3 +63,57 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
63
63
  /escapes the memory root/,
64
64
  );
65
65
  });
66
+
67
+ test("sync pushes commits left behind by an earlier failed push", async (t) => {
68
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
69
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-backlog-"));
70
+ const remote = path.join(tempRoot, "remote.git");
71
+ const lagoon = path.join(tempRoot, "lagoon");
72
+ const projectRoot = path.join(tempRoot, "project");
73
+ const git = (args, cwd = lagoon) => execFileSync("git", args, { cwd }).toString().trim();
74
+
75
+ const identity = { GIT_AUTHOR_NAME: "Test", GIT_AUTHOR_EMAIL: "test@example.com", GIT_COMMITTER_NAME: "Test", GIT_COMMITTER_EMAIL: "test@example.com" };
76
+ const saved = Object.fromEntries(Object.keys(identity).map((key) => [key, process.env[key]]));
77
+ Object.assign(process.env, identity);
78
+ t.after(() => {
79
+ for (const [key, value] of Object.entries(saved)) {
80
+ if (value === undefined) {
81
+ delete process.env[key];
82
+ } else {
83
+ process.env[key] = value;
84
+ }
85
+ }
86
+ });
87
+
88
+ git(["init", "-q", "--bare", "-b", "main", remote], tempRoot);
89
+ git(["clone", "-q", remote, lagoon], tempRoot);
90
+ git(["commit", "-q", "--allow-empty", "-m", "init"]);
91
+ git(["push", "-q", "-u", "origin", "main"]);
92
+ process.env.HIPPOCAMP_GLOBAL_ROOT = lagoon;
93
+ t.after(async () => {
94
+ if (originalRoot === undefined) {
95
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
96
+ } else {
97
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
98
+ }
99
+ await fs.rm(tempRoot, { recursive: true, force: true, maxRetries: 5 });
100
+ });
101
+
102
+ // Simulate an unreachable remote for the first write.
103
+ git(["remote", "set-url", "origin", path.join(tempRoot, "missing.git")]);
104
+ const write = await memory.appendEvent({ content: "Use HTTPS.", keywords: ["sync"], projectRoot, scope: "project", title: "Remote" });
105
+ assert.equal(write.sync.committed, true);
106
+ assert.equal(write.sync.pushed, false);
107
+
108
+ git(["remote", "set-url", "origin", remote]);
109
+ const result = await memory.syncMemory({
110
+ scope: "project",
111
+ projectRoot,
112
+ paths: await memory.getDefaultSyncPaths("project", projectRoot),
113
+ message: "sync",
114
+ });
115
+
116
+ assert.equal(result.committed, false);
117
+ assert.equal(result.pushed, true);
118
+ assert.equal(git(["rev-parse", "main"], remote), git(["rev-parse", "HEAD"]));
119
+ });