@volter/supercode-orchestrator 0.5.85 → 0.5.86

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.
@@ -91,21 +91,23 @@ const SETTLE_MS = 10_000;
91
91
  * write awaits this first. A commit waiting out a busy index is not waited for: it stays owed, said. */
92
92
  export async function settleDocuments(within = SETTLE_MS) {
93
93
  const deadline = Date.now() + within;
94
+ // until none is left: a step that ran may have queued another (a commit its push follows); a retry whose timer fires
95
+ // meanwhile joins the chain and is waited for too
96
+ while (fetching.size || chains.size) {
97
+ const left = deadline - Date.now();
98
+ if (left <= 0) { current().err(`board: ${chains.size + fetching.size} git step(s) of the board documents were still running as this process ended; what they owed stays in the checkout for the next process\n`); break; }
99
+ await Promise.race([Promise.all([...fetching.values(), ...chains.values()]), new Promise((done) => setTimeout(done, left).unref?.())]);
100
+ }
101
+ // then what still waits on a timer, said: after the chains, so a retry a step set while settling is said too
94
102
  for (const boards of boardsAt.values()) for (const board of boards) if (board.retryTimer) {
95
103
  clearTimeout(board.retryTimer); board.retryTimer = null;
96
104
  current().err(`board: ${board.owed.length} write${board.owed.length === 1 ? '' : 's'} of the board documents not committed as this process ends (git would not move HEAD); ${board.owed.length === 1 ? 'it stays' : 'they stay'} owed in the checkout (documents that differ from HEAD) for the next process\n`);
97
105
  }
98
- for (const [root, moves] of filesOwed) if (moves.timer) { clearTimeout(moves.timer); moves.timer = null; current().err(`board: ${moves.files.size} file(s) in ${root} still differ from HEAD as this process ends (what was pushed, not yet written there)\n`); }
106
+ for (const [root, moves] of filesOwed) if (moves.timer) { clearTimeout(moves.timer); moves.timer = null; current().err(`board: ${moves.files.size} file(s) in ${root} still differ from HEAD as this process ends (what was pushed, not yet written there); the next process to open a board there writes them\n`); }
99
107
  for (const [root, sync] of indexOwed) if (sync.timer) {
100
108
  clearTimeout(sync.timer); sync.timer = null;
101
109
  current().err(`board: git's index in ${root} is still locked as this process ends; its board documents' index entries stay behind HEAD until the next process opens the board\n`);
102
110
  }
103
- // until none is left: a step that ran may have queued another (a commit its push follows)
104
- while (fetching.size || chains.size) {
105
- const left = deadline - Date.now();
106
- if (left <= 0) { current().err(`board: ${chains.size + fetching.size} git step(s) of the board documents were still running as this process ended; what they owed stays in the checkout for the next process\n`); return; }
107
- await Promise.race([Promise.all([...fetching.values(), ...chains.values()]), new Promise((done) => setTimeout(done, left).unref?.())]);
108
- }
109
111
  }
110
112
  /** How long a busy index may hold an index sync before it is said (with its age), once per stretch. */
111
113
  const BUSY_SAID_MS = 60_000;
@@ -153,9 +155,28 @@ const filesOwed = new Map();
153
155
  function moveFiles(root, files, from, to) {
154
156
  const moves = filesOwed.get(root) ?? { files: new Map(), since: null, said: false, delay: 0, timer: null };
155
157
  filesOwed.set(root, moves);
156
- for (const file of files) moves.files.set(file, { from, to });
158
+ // a file a second take moves again keeps the base of the first: its working text is still that one's until written
159
+ for (const file of files) moves.files.set(file, { from: moves.files.get(file)?.from ?? from, to });
160
+ keepMoves(root);
157
161
  if (!moves.timer) onChain(root, () => moveNow(root));
158
162
  }
163
+ /** The moves still owed, kept in the checkout's git dir until each is written: a process that ends first leaves them for
164
+ * the next one to open a board there (`derive`). */
165
+ const movesPath = (root) => { const checkout = checkoutOf(root); return checkout ? join(checkout.gitDir, 'supercode-board-moves.json') : null; };
166
+ function keepMoves(root) {
167
+ const path = movesPath(root); if (!path) return;
168
+ const moves = filesOwed.get(root);
169
+ try {
170
+ if (!moves?.files.size) rmSync(path, { force: true });
171
+ else writeAtomically(path, JSON.stringify(Object.fromEntries(moves.files)));
172
+ } catch (error) { current().err(`board: the moves owed in ${root} could not be kept (${error.message}); a process that ends before they are written leaves those files differing from HEAD\n`); }
173
+ }
174
+ /** Moves an earlier process left owed, queued again (once per checkout per process). */
175
+ function deriveMoves(root) {
176
+ const path = movesPath(root); if (!path) return;
177
+ let kept = null; try { kept = JSON.parse(readFileSync(path, 'utf8')); } catch { return; }
178
+ for (const [file, { from, to }] of Object.entries(kept ?? {})) if (from && to) moveFiles(root, [file], from, to);
179
+ }
159
180
  async function moveNow(root) {
160
181
  const moves = filesOwed.get(root);
161
182
  if (!moves) return;
@@ -165,13 +186,26 @@ async function moveNow(root) {
165
186
  const was = (await quiet(root, ['rev-parse', '-q', '--verify', `${from}:${file}`]))?.trim() || null;
166
187
  let exists = true; try { statSync(join(root, file)); } catch { exists = false; }
167
188
  const now = exists ? (await git(root, ['hash-object', '--', file])).trim() : null;
168
- if (now !== was) { current().err(`board: ${file} in ${root} changed here since what was pushed was merged; it is left as it is (HEAD has the pushed text)\n`); moves.files.delete(file); continue; }
189
+ if (now !== was) {
190
+ // already what the take brought (written before a process ended): done; else changed here since, left and said
191
+ const brought = (await quiet(root, ['rev-parse', '-q', '--verify', `${to}:${file}`]))?.trim() || null;
192
+ if (now !== brought) current().err(`board: ${file} in ${root} changed here since what was pushed was merged; it is left as it is (HEAD has the pushed text)\n`);
193
+ // written before a process ended whose index sync was still owed: the index entry follows HEAD now (a file HEAD
194
+ // lacks leaves the index, so it is not left staged against HEAD)
195
+ else await git(root, ['reset', '-q', 'HEAD', '--', file]);
196
+ moves.files.delete(file); continue;
197
+ }
169
198
  const kept = Boolean((await quiet(root, ['rev-parse', '-q', '--verify', `${to}:${file}`]))?.trim());
170
199
  if (kept) await git(root, ['checkout', to, '--', file]);
171
- else rmSync(join(root, file), { force: true });
200
+ else {
201
+ // the index entry first (a busy index retries with the file still in place), then the file
202
+ await git(root, ['rm', '--cached', '-q', '--ignore-unmatch', '--', file]);
203
+ rmSync(join(root, file), { force: true });
204
+ }
172
205
  moves.files.delete(file);
173
206
  } catch (error) { failed ??= error; }
174
207
  }
208
+ keepMoves(root);
175
209
  if (!moves.files.size) { filesOwed.delete(root); return; }
176
210
  moves.since ??= Date.now();
177
211
  if (!moves.said && Date.now() - moves.since >= BUSY_SAID_MS) {
@@ -234,6 +268,7 @@ export class GitDocument {
234
268
  syncIndex(this.root, this.paths.map((path) => this.rel(path)));
235
269
  if (pushDerived.has(this.root)) return;
236
270
  pushDerived.add(this.root);
271
+ deriveMoves(this.root);
237
272
  const branch = this.branch();
238
273
  if (branch) onChain(this.root, async () => { if (await this.unpushed(branch) && !this.pushing) this.pushInBackground(branch); });
239
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.85",
3
+ "version": "0.5.86",
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": {