@volter/supercode-orchestrator 0.5.14 → 0.5.15

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.
@@ -2192,6 +2192,9 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2192
2192
  try {
2193
2193
  board = openBoard(root, slug);
2194
2194
  board.label = boardLabel(root, slug);
2195
+ // A board document committed by a write whose push the remote refused (or never answered) is published by the
2196
+ // round, not left for the next write: a quiet board still reaches its remote.
2197
+ try { board.publishPending?.(); } catch (error) { report.queued.push(['board document', `push pending: ${error.message}`]); }
2195
2198
  // what the dispatcher records on its own is the board's act
2196
2199
  setActor(boardAddress(board.label));
2197
2200
  // where the tick's time goes, phase by phase (ms), printed by `serve --verbose` and in `--json`
@@ -6,8 +6,8 @@
6
6
  // the two texts of a board document merged by ztrack's own document merge, in which a hand edit wins its cards. Nothing
7
7
  // either side wrote is dropped; a merge that cannot be made stops the push and says why, leaving both texts in the
8
8
  // checkout's history.
9
- import { execFileSync } from 'node:child_process';
10
- import { readFileSync, writeFileSync } from 'node:fs';
9
+ import { execFileSync, spawn } from 'node:child_process';
10
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
11
11
  import { basename, dirname, relative } from 'node:path';
12
12
 
13
13
  const git = (cwd, args, input) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'], ...(input === undefined ? {} : { input }), timeout: 120_000 });
@@ -23,6 +23,10 @@ const indexed = (cwd, args) => {
23
23
  }
24
24
  };
25
25
 
26
+ const FETCH_EVERY_MS = 15_000;
27
+ /** When this checkout last asked its remote (git writes FETCH_HEAD on every fetch, whichever process ran it). */
28
+ const fetchedAt = (root) => { try { return statSync(git(root, ['rev-parse', '--git-path', 'FETCH_HEAD']).trim().replace(/^(?!\/)/, `${root}/`)).mtimeMs; } catch { return 0; } };
29
+
26
30
  export class GitDocument {
27
31
  constructor(paths, merge) {
28
32
  this.paths = paths;
@@ -46,11 +50,16 @@ export class GitDocument {
46
50
  this.warnedDetached = true;
47
51
  }
48
52
 
49
- // Before a write: take what was pushed, so a merged pull request's edit is the document the write starts from.
53
+ // Before a write: take what was pushed, so a merged pull request's edit is the document the write starts from. The
54
+ // remote is asked at most every FETCH_EVERY_MS (git's own FETCH_HEAD is the stamp, so every board process shares it):
55
+ // a fetch on every write put a network round trip on each one. A checkout holding commits its background push did
56
+ // not land (refused: something was pushed meanwhile) takes what was pushed now and pushes before writing again.
50
57
  pull() {
51
58
  if (!this.usable) return;
52
59
  const branch = this.branch();
53
60
  if (!branch) return this.detached();
61
+ if (this.unpushed(branch)) return this.pushing ? undefined : this.publish(branch, 'board');
62
+ if (Date.now() - fetchedAt(this.root) < FETCH_EVERY_MS) return;
54
63
  if (quiet(this.root, ['fetch', '--quiet', 'origin', branch]) === null) return; // offline: the next write tries again
55
64
  const behind = Number(git(this.root, ['rev-list', '--count', `HEAD..origin/${branch}`]).trim());
56
65
  if (behind === 0) return;
@@ -72,6 +81,33 @@ export class GitDocument {
72
81
  indexed(this.root, ['-c', `user.name=${who}`, '-c', 'user.email=board@supercode.invalid', 'commit', '--quiet', '--author', `${who} <board@supercode.invalid>`, '-m', message, '--', ...files]);
73
82
  const branch = this.branch();
74
83
  if (!branch) return this.detached();
84
+ // The push leaves the write's path: the commit is the write, and a push the remote refuses is taken up by the next
85
+ // write's pull (unpushed commits), which merges what was pushed and pushes before it writes.
86
+ // While it runs, this process's next write does not push the same commits beside it.
87
+ const pushing = spawn('git', ['push', '--quiet', 'origin', `HEAD:${branch}`], { cwd: this.root, stdio: 'ignore', detached: true });
88
+ this.pushing = pushing;
89
+ const done = () => { if (this.pushing === pushing) this.pushing = null; };
90
+ pushing.on('error', done); pushing.on('exit', done);
91
+ pushing.unref();
92
+ }
93
+
94
+ /** The board's round: publish what an earlier write committed and the remote has not taken (a refused or failed
95
+ * background push), with the same merge and three attempts a write's pull uses. */
96
+ publishPending() {
97
+ if (!this.usable || this.pushing) return;
98
+ const branch = this.branch();
99
+ if (!branch || !this.unpushed(branch)) return;
100
+ if (quiet(this.root, ['fetch', '--quiet', 'origin', branch]) === null) return; // offline: the next round tries again
101
+ if (this.unpushed(branch)) this.publish(branch, 'board');
102
+ }
103
+
104
+ /** Whether this checkout holds board commits the remote does not have. */
105
+ unpushed(branch) {
106
+ return Number(quiet(this.root, ['rev-list', '--count', `origin/${branch}..HEAD`])?.trim() || 0) > 0;
107
+ }
108
+
109
+ /** Push what this checkout holds, taking anything pushed meanwhile. */
110
+ publish(branch, who) {
75
111
  for (let attempt = 0; attempt < 3; attempt++) {
76
112
  if (quiet(this.root, ['push', '--quiet', 'origin', `HEAD:${branch}`]) !== null) return;
77
113
  if (quiet(this.root, ['fetch', '--quiet', 'origin', branch]) === null) throw new Error('the board documents were committed here but could not be pushed: the remote did not answer');
package/board/ztrack.mjs CHANGED
@@ -69,7 +69,7 @@ export function openZtrackBoard(marker, { create = false, document = null, seed
69
69
  const backing = new ZtrackBoard({ ...config, seed, marker, init, root, slug, compatPath });
70
70
  // A board made here is in Hermes's kanban.db at once, not after its first write.
71
71
  if (create) backing.exportFollowing();
72
- return { db: backing.db, tx: (fn) => backing.tx(fn), close: () => backing.close(), document: backing.path };
72
+ return { db: backing.db, tx: (fn) => backing.tx(fn), publishPending: () => backing.git?.publishPending(), close: () => backing.close(), document: backing.path };
73
73
  }
74
74
 
75
75
  class ZtrackBoard {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {