mainflow 0.44.0-alpha.1 → 0.44.0-alpha.2

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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { P as UNTIL_FORMS, X as parseUntil, Y as parseRef, Z as refExample, ct as stampMs, it as untilProblem, n as CLOSED_STATUS } from "./dist-CXrUfRQp.js";
3
3
  import { $t as realPath, Bt as notRegisteredMessage, Ft as requireSlug, Gt as resolveHub, Ht as registerHub, It as hubAtRoot, Jt as nodeStorage, Kt as unregisterHub, Lt as hubHoldingFile, Mt as MAINLINE_FILE, O as loadSessions, Pt as TASK_FILE, Q as HubError, S as sessionBriefing, Ut as registeredName, Vt as readRegistry, Wt as registryFile, X as applyOperation, Xt as dataFile, Z as checkHubName, b as untilState, d as migrateHubData, f as migrateHub, ft as nowMinute, g as RETRY_RULES, gt as BLOB_HASH, h as receiverFromEnv, k as readSession, lt as remoteMembers, m as retroReportFromReceiver, nn as DATA_DIR, p as retroReport, qt as userDataDir, rn as WriteError, rt as sectionText, st as memberOfGroup, tt as loadHub, u as groupPlan, ut as unavailableHubs, vt as MAX_CHANGE_PAGE, x as duplicateClaimLines, y as initHub, yt as MAX_FILE_BYTES, zt as listHubs } from "./host-C74j2Qox.js";
4
4
  import { t as listProgress } from "./progress-1BfPg2zN.js";
5
- import { A as hubName, C as readRemote, D as Unreachable, E as ServerError, F as syncPaths, M as SETTINGS_FILE, O as ENV, T as ServerApi, _ as DaemonUnavailable, a as versionGate, b as readDaemon, c as oversizedFiles, d as planAnnounce, f as planFetch, g as DaemonRunning, h as startDaemon, i as requireWritableVersion, j as syncSettings, k as earlierToken, l as fetchDenial, m as toolCallOf, n as hubWriter, o as queuedCount, p as planFlush, r as writtenLines, s as remoteMode, t as CONFIRM_MS, u as hookAnswer, v as callDaemon, w as HubRewritingError, x as silentHint, y as probeDaemon } from "./operation-BmCeGKHw.js";
5
+ import { A as hubName, C as readRemote, D as Unreachable, E as ServerError, F as syncPaths, M as SETTINGS_FILE, O as ENV, T as ServerApi, _ as DaemonUnavailable, a as versionGate, b as readDaemon, c as oversizedFiles, d as planAnnounce, f as planFetch, g as DaemonRunning, h as startDaemon, i as requireWritableVersion, j as syncSettings, k as earlierToken, l as fetchDenial, m as toolCallOf, n as hubWriter, o as queuedCount, p as planFlush, r as writtenLines, s as remoteMode, t as CONFIRM_MS, u as hookAnswer, v as callDaemon, w as HubRewritingError, x as silentHint, y as probeDaemon } from "./operation-CUeOFEaf.js";
6
6
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
7
7
  import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
8
8
  import { hookFallback, parsePayload, runHook } from "./hooks.js";
@@ -1,6 +1,6 @@
1
1
  import { D as SESSION_ID, I as configSchema, ft as describeRequest, gt as hasControl, tt as repoPaths } from "./dist-CXrUfRQp.js";
2
2
  import { $t as realPath, Gt as resolveHub, Jt as nodeStorage, M as matchGrants, Ot as formatMismatch, Yt as currentBranch, Zt as gitCheckout, j as grantScope, k as readSession, kt as hubFormatVersion, mt as readYaml } from "./host-C74j2Qox.js";
3
- import { n as hubWriter, r as writtenLines } from "./operation-BmCeGKHw.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation-CUeOFEaf.js";
4
4
  import { OWNER_REPO, githubOrigin, quote, readManifest, resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
5
5
  import { claudeCodeSessionId, parsePayload } from "./hooks.js";
6
6
  import { readFileSync } from "node:fs";
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { I as configSchema, Y as parseRef, ft as describeRequest, rt as taskSchema, tt as repoPaths } from "./dist-CXrUfRQp.js";
2
2
  import { $ as isOpenMainline, $t as realPath, Bt as notRegisteredMessage, C as turnReminder, F as headerLine, Gt as resolveHub, I as mayDecorate, Jt as nodeStorage, Lt as hubHoldingFile, M as matchGrants, N as decorateReply, Nt as SESSIONS_DIR, O as loadSessions, Ot as formatMismatch, P as endsWithMarker, Q as HubError, Qt as hubPath, Rt as inRepositoryHubAt, S as sessionBriefing, Xt as dataFile, Yt as currentBranch, dt as taskFile, en as worktreesOf, i as hostName, j as grantScope, k as readSession, kt as hubFormatVersion, mt as readYaml, nn as DATA_DIR, nt as loadMainlines, pt as readFrontMatter, tt as loadHub } from "./host-C74j2Qox.js";
3
- import { n as hubWriter, r as writtenLines } from "./operation-BmCeGKHw.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation-CUeOFEaf.js";
4
4
  import { SCENARIO_LINES, SCENARIO_SKIP_KINDS } from "./facts.js";
5
5
  import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
6
6
  import { createHash } from "node:crypto";
package/dist/launch.js CHANGED
@@ -273,7 +273,7 @@ async function launchUi(dirs, hub, configured, options) {
273
273
  }
274
274
  let server;
275
275
  if (!reuse) {
276
- const { serveUi } = await import("./ui-8NTeAfC7.js");
276
+ const { serveUi } = await import("./ui-qqVCewFz.js");
277
277
  server = await serveUi(dirs, choice.port);
278
278
  writeUiRecord(dirs, {
279
279
  port: choice.port,
@@ -920,6 +920,11 @@ const readKnownDirs = (hub) => {
920
920
  const dirs = readJson(syncPaths(hub).known, {}).dirs;
921
921
  return Array.isArray(dirs) ? dirs.filter((dir) => typeof dir === "string") : [];
922
922
  };
923
+ /** The hub's seq known.json names (KnownFile.seq); undefined when it names none, or none that is a seq. */
924
+ const readKnownSeq = (hub) => {
925
+ const seq = readJson(syncPaths(hub).known, {}).seq;
926
+ return Number.isInteger(seq) && seq >= 0 ? seq : void 0;
927
+ };
923
928
  /** What known.json writes before a binary file's sha256, apart from a text's content hash. */
924
929
  const BLOB_PREFIX = "blob:";
925
930
  /** The text of the version known.json names by `hash`; undefined when it was not kept, as by a sync client before known/. */
@@ -936,9 +941,9 @@ function readKnownText(dirs, hash) {
936
941
  * there yet; the texts of versions it no longer names are removed. A version whose text the client does not have keeps
937
942
  * the text known/ holds, if any. The new texts go first, then known.json, then the removals, so a client killed at any
938
943
  * step leaves a known.json whose every text is in known/. A binary file is given as its sha256 with BLOB_PREFIX, and no text.
939
- * `dirs` are the directories it names.
944
+ * `dirs` are the directories it names, and `seq` the hub's seq they are as of, written with them (KnownFile.seq).
940
945
  */
941
- function writeKnown(hub, files, dirs = []) {
946
+ function writeKnown(hub, files, dirs = [], seq) {
942
947
  const dir = syncPaths(hub).knownTexts;
943
948
  mkdirSync(dir, { recursive: true });
944
949
  const named = new Set(Object.values(files).map((file) => file.hash));
@@ -951,7 +956,8 @@ function writeKnown(hub, files, dirs = []) {
951
956
  }
952
957
  const known = {
953
958
  files: Object.fromEntries(Object.entries(files).map(([path, file]) => [path, file.hash])),
954
- ...dirs.length > 0 ? { dirs } : {}
959
+ ...dirs.length > 0 ? { dirs } : {},
960
+ ...seq === void 0 ? {} : { seq }
955
961
  };
956
962
  writeJson(syncPaths(hub).known, known);
957
963
  for (const name of held) if (!named.has(name)) rmSync(join(dir, name), { force: true });
@@ -1656,7 +1662,8 @@ var SyncClient = class extends EventEmitter {
1656
1662
  * every directory, but for the paths changed after it: a whole snapshot with binary files was taken in this run, and
1657
1663
  * nothing since made the client unsure of what it holds, as a server that stopped keeping binary files, or a hub
1658
1664
  * reloaded. Only then is a snapshot asked for what changed since hubSeq (`since`), as a path the answer does not name
1659
- * holds what the client takes the server to hold. Not kept across runs: known.json names no seq.
1665
+ * holds what the client takes the server to hold. A run after it starts from known.json instead, which names the seq
1666
+ * (knownSeq, resumeSeq).
1660
1667
  */
1661
1668
  holdsHub = false;
1662
1669
  /**
@@ -1664,6 +1671,20 @@ var SyncClient = class extends EventEmitter {
1664
1671
  * not leave the client holding the hub (holdsHub), whatever it took in, so the next one is whole.
1665
1672
  */
1666
1673
  reloads = 0;
1674
+ /**
1675
+ * The hub's seq known.json named when this run started (readKnownSeq): the files it names, its directories and
1676
+ * remote.json are the hub as of it, so the run's first snapshot asks for what changed since, as one after a reconnect
1677
+ * does (holdsHub). Undefined once a snapshot was taken, or when known.json names none or the run cannot take it, as
1678
+ * when a text it names is not in known/.
1679
+ */
1680
+ resumeSeq;
1681
+ /**
1682
+ * For each path, a seq at or before which the hub held another version than the one the client knows of now (in
1683
+ * `cloud`, or `remote`): a broadcast's own seq, or, for a snapshot's, one after the seq of the version before it. A
1684
+ * path whose known.json version lags behind is changed on the server after the seq known.json is written with
1685
+ * (knownSeq), so what changed since names it.
1686
+ */
1687
+ changedAt = /* @__PURE__ */ new Map();
1667
1688
  /** The seqs of the broadcasts on the current connection above hubSeq with a change between that never arrived */
1668
1689
  ahead = /* @__PURE__ */ new Set();
1669
1690
  waiters = /* @__PURE__ */ new Set();
@@ -1861,6 +1882,7 @@ var SyncClient = class extends EventEmitter {
1861
1882
  async restoreKnown() {
1862
1883
  this.copied = !existsSync(this.paths.known);
1863
1884
  this.blobsCompared = !this.copied && !existsSync(this.paths.copyBlobs);
1885
+ this.resumeSeq = this.blobsCompared ? readKnownSeq(this.dirs) : void 0;
1864
1886
  for (const dir of readKnownDirs(this.dirs)) this.syncedDirs.add(dir);
1865
1887
  for (const [path, hash] of Object.entries(readKnown(this.dirs))) {
1866
1888
  if (hash.startsWith("blob:")) {
@@ -1884,7 +1906,9 @@ var SyncClient = class extends EventEmitter {
1884
1906
  text,
1885
1907
  server: text
1886
1908
  });
1909
+ else if (this.syncs(path)) this.resumeSeq = void 0;
1887
1910
  }
1911
+ if (this.resumeSeq !== void 0) this.log(`known.json is of the hub's seq ${this.resumeSeq}: the first snapshot asks for what changed since`);
1888
1912
  }
1889
1913
  /**
1890
1914
  * The refusals a run killed before it settled them left in the outbox (src/state.ts): each text not saved yet is saved,
@@ -2118,12 +2142,13 @@ var SyncClient = class extends EventEmitter {
2118
2142
  if ("reload" in message && message.reload) {
2119
2143
  this.reloads++;
2120
2144
  this.holdsHub = false;
2145
+ this.resumeSeq = void 0;
2121
2146
  this.track(this.resync());
2122
2147
  return;
2123
2148
  }
2124
2149
  for (const event of message.files) if (event.kind === "dir") this.receiveDir(event.path, seq, true);
2125
2150
  else if (event.kind === "dirRemoved") this.receiveDirRemoved(event.path, seq);
2126
- else this.receive(event, seq);
2151
+ else this.receive(event, seq, true);
2127
2152
  if (this.ahead.size > 0 && this.synced && this.resyncing === null) {
2128
2153
  this.log(`missed the hub's changes ${this.hubSeq + 1} to ${Math.min(...this.ahead) - 1}; reloading the snapshot`);
2129
2154
  this.track(this.resync());
@@ -2144,7 +2169,7 @@ var SyncClient = class extends EventEmitter {
2144
2169
  * that may stay on the server, as a new screenshot of a closed mainline, goes into `remote` instead of being downloaded,
2145
2170
  * as a listed text does in a snapshot.
2146
2171
  */
2147
- receive(event, seq) {
2172
+ receive(event, seq, live = false) {
2148
2173
  if (!this.syncs(event.path)) return;
2149
2174
  const file = event.kind === "file" ? event.text : event.kind === "blob" ? {
2150
2175
  kind: "blob",
@@ -2157,6 +2182,7 @@ var SyncClient = class extends EventEmitter {
2157
2182
  this.trace(`older ${event.path} ${label} seq ${seq}, holding seq ${held}`);
2158
2183
  return;
2159
2184
  }
2185
+ this.noteChange(event.path, file === null ? null : serverHash(file), live ? seq : held + 1);
2160
2186
  this.fileSeq.set(event.path, seq);
2161
2187
  if (this.downloadErrors.has(event.path)) {
2162
2188
  const before = this.cloud.get(event.path);
@@ -2238,6 +2264,16 @@ var SyncClient = class extends EventEmitter {
2238
2264
  forgetDir(path) {
2239
2265
  if (this.syncedDirs.delete(path)) this.saveKnown();
2240
2266
  }
2267
+ /** The hash the client knows the server's file at `path` by: of its version in `cloud`, or `remote`; null for none. */
2268
+ serverView(path) {
2269
+ const file = this.cloud.get(path);
2270
+ if (file !== void 0 && file !== null) return serverHash(file);
2271
+ return this.remote.get(path) ?? null;
2272
+ }
2273
+ /** The server's file at `path` is to be known by `hash` (null for none), a change of the hub at seq `at` or later (changedAt). */
2274
+ noteChange(path, hash, at) {
2275
+ if (this.serverView(path) !== hash) this.changedAt.set(path, at);
2276
+ }
2241
2277
  /** Whether the client holds `path` as of the hub's `seq` or later. */
2242
2278
  reached(path, seq) {
2243
2279
  return this.hubSeq >= seq || (this.fileSeq.get(path) ?? -1) >= seq;
@@ -2381,7 +2417,8 @@ var SyncClient = class extends EventEmitter {
2381
2417
  * since hubSeq alone (`since`). A server whose change log tells answers with the paths changed (SnapshotDelta): taken
2382
2418
  * in over what the client holds once they make the hub's digest (changesHold, takeChanges), and else dropped for the
2383
2419
  * whole snapshot. A server that cannot tell, or one from before it, answers the first batch, and the snapshot goes on
2384
- * whole as ever.
2420
+ * whole as ever. A run's first snapshot asks the same way from the seq known.json names (resumeSeq), taking known.json,
2421
+ * remote.json and the directories known.json names for what the server held as of it (knownCopy).
2385
2422
  */
2386
2423
  takeSnapshot() {
2387
2424
  return this.exclusive(() => this.snapshotNow());
@@ -2406,13 +2443,15 @@ var SyncClient = class extends EventEmitter {
2406
2443
  /** The reload broadcasts that came before this snapshot: one that comes while it runs leaves the next one whole */
2407
2444
  const reloads = this.reloads;
2408
2445
  /**
2409
- * Whether the snapshot may ask for what changed since hubSeq, read once the probe told whether the server keeps
2446
+ * Whether the snapshot may ask for what changed since a seq: hubSeq while the client holds the hub, or the seq
2447
+ * known.json names before the run's first snapshot (resumeSeq). Read once the probe told whether the server keeps
2410
2448
  * binary files; not once an answer of it failed the digest
2411
2449
  */
2412
- let askChanges = this.holdsHub;
2450
+ let askChanges = this.holdsHub || this.resumeSeq !== void 0;
2413
2451
  for (;;) {
2414
2452
  before = this.hubSeq;
2415
- const base = askChanges && this.blobSupport === true && before >= 0 ? this.serverCopy() : null;
2453
+ const from = !askChanges || this.blobSupport !== true ? null : this.holdsHub ? before >= 0 ? before : null : this.resumeSeq ?? null;
2454
+ const base = from === null ? null : this.holdsHub ? this.serverCopy() : this.knownCopy();
2416
2455
  ({texts: held, blobs: heldBlobs} = await this.holdings());
2417
2456
  let dropped = false;
2418
2457
  for (const path of this.remote.keys()) {
@@ -2433,7 +2472,7 @@ var SyncClient = class extends EventEmitter {
2433
2472
  value: hubDigest(have, dirs),
2434
2473
  dirs
2435
2474
  } : void 0;
2436
- since = base !== null && withBlobs && !dropped ? before : void 0;
2475
+ since = base !== null && from !== null && withBlobs && !dropped ? from : void 0;
2437
2476
  const taken = await this.api.snapshot(have, {
2438
2477
  listOnly: this.listOnly(this.copied && !this.synced),
2439
2478
  blobs: withBlobs,
@@ -2583,6 +2622,8 @@ var SyncClient = class extends EventEmitter {
2583
2622
  this.takenUpTo(seq);
2584
2623
  this.synced = true;
2585
2624
  this.holdsHub = withBlobs && this.reloads === reloads;
2625
+ this.resumeSeq = void 0;
2626
+ this.saveKnown();
2586
2627
  if (withBlobs && !this.blobsCompared) {
2587
2628
  this.blobsCompared = true;
2588
2629
  rmSync(this.paths.copyBlobs, { force: true });
@@ -2597,6 +2638,32 @@ var SyncClient = class extends EventEmitter {
2597
2638
  await this.queueRemovedDirs();
2598
2639
  }
2599
2640
  /**
2641
+ * What the client takes the server to hold as of the seq known.json names, before the run's first snapshot
2642
+ * (resumeSeq): the version known.json names of each file, but one only on the server (remote.json), and the
2643
+ * directories it names; with what a broadcast taken in since the run started holds, which is of a later seq.
2644
+ */
2645
+ knownCopy() {
2646
+ const files = /* @__PURE__ */ new Map();
2647
+ for (const [path, disk] of this.confirmed) {
2648
+ if (!disk || !this.syncs(path) || this.remote.has(path)) continue;
2649
+ if (disk.blob) files.set(path, {
2650
+ kind: "blob",
2651
+ hash: disk.hash
2652
+ });
2653
+ else if (disk.text !== void 0) files.set(path, disk.text);
2654
+ }
2655
+ for (const [path, file] of this.cloud) {
2656
+ if (!this.syncs(path)) continue;
2657
+ if (file === null) files.delete(path);
2658
+ else files.set(path, file);
2659
+ }
2660
+ return {
2661
+ files,
2662
+ remote: new Map(this.remote),
2663
+ dirs: new Set(this.syncedDirs)
2664
+ };
2665
+ }
2666
+ /**
2600
2667
  * What the client takes the server to hold, while it holds the hub (holdsHub): the server's version of each file it
2601
2668
  * took in, a binary file's included, the hash of each file only on the server, and the directories the server holds.
2602
2669
  * A file here the server never had is not among them, though a snapshot request names it.
@@ -3237,7 +3304,9 @@ var SyncClient = class extends EventEmitter {
3237
3304
  }
3238
3305
  /** Note `path` as only on the server, with the hash of the server's text as of `seq`, unless a newer version came. */
3239
3306
  keepRemote(path, hash, seq) {
3240
- if (seq <= (this.fileSeq.get(path) ?? -1)) return;
3307
+ const held = this.fileSeq.get(path) ?? -1;
3308
+ if (seq <= held) return;
3309
+ this.noteChange(path, hash, held + 1);
3241
3310
  this.fileSeq.set(path, seq);
3242
3311
  this.remote.set(path, hash);
3243
3312
  this.cloud.delete(path);
@@ -5688,12 +5757,39 @@ var SyncClient = class extends EventEmitter {
5688
5757
  this.knownTimer = void 0;
5689
5758
  const files = this.knownFiles();
5690
5759
  const dirs = this.knownDirs();
5760
+ const seq = this.knownSeq();
5761
+ if (seq !== void 0) this.saveRemoteNow(final);
5691
5762
  try {
5692
- underLock(this.dirs, () => writeKnown(this.dirs, files, dirs));
5763
+ underLock(this.dirs, () => writeKnown(this.dirs, files, dirs, seq));
5693
5764
  } catch (error) {
5694
5765
  this.log(`could not save local/sync/known.json: ${error.message}`);
5695
5766
  }
5696
5767
  }
5768
+ /**
5769
+ * The hub's seq known.json is written with (KnownFile.seq): one as of which the files it names, its directories and
5770
+ * remote.json are the hub's, but for paths changed on the server after it, so that what changed since names every
5771
+ * path whose version there differs. Only while the client holds the hub (holdsHub) and no snapshot is under way, as
5772
+ * the files it is taking in are of later seqs; and none while a listed file is being downloaded. It is hubSeq, or
5773
+ * lower when a file's version known.json names is not the server's the client knows of, as one the outbox holds a
5774
+ * write about while the server changed it: one before that change (changedAt). Undefined when there is none.
5775
+ */
5776
+ knownSeq() {
5777
+ if (!this.holdsHub || this.hubSeq < 0 || this.resyncing !== null || this.awaiting.size > 0) return void 0;
5778
+ const listed = this.listedRemote();
5779
+ let seq = this.hubSeq;
5780
+ for (const path of /* @__PURE__ */ new Set([
5781
+ ...this.cloud.keys(),
5782
+ ...this.confirmed.keys(),
5783
+ ...this.remote.keys()
5784
+ ])) {
5785
+ if (!this.syncs(path)) continue;
5786
+ const disk = this.confirmed.get(path);
5787
+ if ((listed.get(path) ?? (disk ? disk.hash : null)) === this.serverView(path)) continue;
5788
+ seq = Math.min(seq, (this.changedAt.get(path) ?? 0) - 1);
5789
+ if (seq < 0) return void 0;
5790
+ }
5791
+ return seq;
5792
+ }
5697
5793
  };
5698
5794
  //#endregion
5699
5795
  //#region ../sync/dist/ipc.js
@@ -5724,30 +5820,75 @@ var DaemonUnavailable = class extends Error {
5724
5820
  };
5725
5821
  /** Another sync client already runs for the hub. */
5726
5822
  var DaemonRunning = class extends Error {};
5823
+ /** The answer's body could not be read to its end, after its status came. */
5824
+ var BodyUnread = class extends Error {};
5825
+ /**
5826
+ * One request to the sync client on the loopback interface, with the status and the whole body of its answer. It uses
5827
+ * node:http, taken when first called, rather than fetch: fetch loads undici and with it tls, and a process with
5828
+ * NODE_USE_SYSTEM_CA set reads the system's certificates when tls loads, which costs every hook that writes about 140 ms.
5829
+ * `agent: false` opens a connection for this request alone, so no kept-alive socket holds a short-lived command open.
5830
+ * No answer within `timeoutMs` rejects with an error named TimeoutError, which callDaemon takes as a silent sync client;
5831
+ * the time running out while the body is read rejects with BodyUnread, as does any failure reading it.
5832
+ */
5833
+ function loopbackRequest(port, path, headers, body, timeoutMs) {
5834
+ return new Promise((resolve, reject) => {
5835
+ let answered = false;
5836
+ const request = process.getBuiltinModule("node:http").request({
5837
+ host: "127.0.0.1",
5838
+ port,
5839
+ path,
5840
+ method: body === void 0 ? "GET" : "POST",
5841
+ headers,
5842
+ agent: false
5843
+ }, (response) => {
5844
+ answered = true;
5845
+ const chunks = [];
5846
+ response.on("data", (chunk) => chunks.push(chunk));
5847
+ response.on("end", () => {
5848
+ clearTimeout(timer);
5849
+ resolve({
5850
+ status: response.statusCode ?? 0,
5851
+ text: Buffer.concat(chunks).toString("utf8")
5852
+ });
5853
+ });
5854
+ response.on("error", (error) => {
5855
+ clearTimeout(timer);
5856
+ reject(new BodyUnread(error.message));
5857
+ });
5858
+ });
5859
+ const timer = setTimeout(() => {
5860
+ const error = answered ? new BodyUnread("the answer did not end in time") : Object.assign(/* @__PURE__ */ new Error("The operation was aborted due to timeout"), { name: "TimeoutError" });
5861
+ request.destroy(error);
5862
+ reject(error);
5863
+ }, timeoutMs);
5864
+ request.on("error", (error) => {
5865
+ clearTimeout(timer);
5866
+ reject(answered ? new BodyUnread(error.message) : error);
5867
+ });
5868
+ request.end(body);
5869
+ });
5870
+ }
5727
5871
  async function callDaemon(daemon, route, body, timeoutMs = 6e4) {
5872
+ const notSync = () => new DaemonUnavailable(`what answers on port ${daemon.port} is not mainflow sync (pid ${daemon.pid})`);
5728
5873
  let response;
5729
5874
  try {
5730
- response = await fetch(`http://127.0.0.1:${daemon.port}${route}`, {
5731
- method: body === void 0 ? "GET" : "POST",
5732
- headers: {
5733
- [SECRET_HEADER]: daemon.secret,
5734
- "content-type": "application/json"
5735
- },
5736
- body: body === void 0 ? void 0 : JSON.stringify(body),
5737
- signal: AbortSignal.timeout(timeoutMs)
5738
- });
5875
+ response = await loopbackRequest(daemon.port, route, {
5876
+ [SECRET_HEADER]: daemon.secret,
5877
+ "content-type": "application/json"
5878
+ }, body === void 0 ? void 0 : JSON.stringify(body), timeoutMs);
5739
5879
  } catch (error) {
5880
+ if (error instanceof BodyUnread) throw notSync();
5740
5881
  const silent = error.name === "TimeoutError";
5741
5882
  throw new DaemonUnavailable(`mainflow sync (pid ${daemon.pid}) does not answer: ${error.message}`, silent);
5742
5883
  }
5743
5884
  let parsed;
5744
5885
  try {
5745
- parsed = await response.json();
5886
+ parsed = JSON.parse(response.text);
5746
5887
  } catch {
5747
- throw new DaemonUnavailable(`what answers on port ${daemon.port} is not mainflow sync (pid ${daemon.pid})`);
5888
+ throw notSync();
5748
5889
  }
5749
5890
  if (response.status === 403) throw new DaemonUnavailable(`what answers on port ${daemon.port} is not this hub's mainflow sync (pid ${daemon.pid})`);
5750
- if (!response.ok) throw new Error(`mainflow sync failed: ${parsed.error}`);
5891
+ if (response.status < 200 || response.status > 299) throw new Error(`mainflow sync failed: ${parsed.error}`);
5751
5892
  return parsed;
5752
5893
  }
5753
5894
  /** How long a probe waits for the sync client's answer; a client holding the write lock answers after it. */
@@ -1,7 +1,7 @@
1
1
  import { I as configSchema, tt as repoPaths } from "./dist-CXrUfRQp.js";
2
2
  import { A as transcriptPath, D as readNotices$1, E as parseWarn, It as hubAtRoot, L as attributeTrace, O as loadSessions, Q as HubError, Qt as hubPath, R as traceTranscript, T as markNoticesHandled, Xt as dataFile, _ as ARCHIVE_KINDS, a as openInApp, at as mainlineEdges, ct as refIndex, et as loadConfig, it as dependencyGraph, k as readSession, lt as remoteMembers, mt as readYaml, nn as DATA_DIR, ot as linksInto, rn as WriteError, rt as sectionText, tn as ConflictError, tt as loadHub, u as groupPlan, v as setArchivedMany, w as HANDLED_FILE, z as transcriptTitle } from "./host-C74j2Qox.js";
3
3
  import { t as listProgress } from "./progress-1BfPg2zN.js";
4
- import { C as readRemote, F as syncPaths, N as SYNC_DIR, P as isSynced, S as watchTree, _ as DaemonUnavailable, b as readDaemon, n as hubWriter, r as writtenLines, s as remoteMode, v as callDaemon } from "./operation-BmCeGKHw.js";
4
+ import { C as readRemote, F as syncPaths, N as SYNC_DIR, P as isSynced, S as watchTree, _ as DaemonUnavailable, b as readDaemon, n as hubWriter, r as writtenLines, s as remoteMode, v as callDaemon } from "./operation-CUeOFEaf.js";
5
5
  import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
6
6
  import { readUiRecord, realRoot } from "./launch.js";
7
7
  import { createRequire } from "node:module";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mainflow",
3
- "version": "0.44.0-alpha.1",
3
+ "version": "0.44.0-alpha.2",
4
4
  "description": "本機多 AI 協作的主脈絡任務管理:CLI 與介面",
5
5
  "license": "MIT",
6
6
  "type": "module",