@code-yeongyu/senpi 2026.10.23-1 → 2026.10.24-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.
Files changed (49) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/dist/bundle/chunks/{chunk-TFFJEEHQ.js → chunk-3PTVLFBA.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-52DQGTJR.js → chunk-CXED7XG5.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-TCWZBUBG.js → chunk-FKXOOQMF.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-7R3FUPMO.js → chunk-OS42QW66.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-NYYDXANA.js → chunk-QEHPPMVF.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-TFULKHNU.js → chunk-T75A2MVF.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-E6IDMANN.js → chunk-XE753Y6U.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-RWRICRZB.js → chunk-ZSU4SPDQ.js} +80 -80
  10. package/dist/bundle/chunks/{cli-main-FVJRIDEL.js → cli-main-DQBJUCQS.js} +1 -1
  11. package/dist/bundle/chunks/{host-session-command-I7W5WSGN.js → host-session-command-MMYPIHCX.js} +1 -1
  12. package/dist/bundle/chunks/{interactive-mode-2GTQWY5I.js → interactive-mode-KIYNWS2N.js} +1 -1
  13. package/dist/bundle/chunks/{multi-session-host-4DDNG3S4.js → multi-session-host-X2XCJ7BZ.js} +4 -4
  14. package/dist/bundle/chunks/{package-manager-cli-VMY6B2WQ.js → package-manager-cli-UQMDWNHH.js} +1 -1
  15. package/dist/bundle/chunks/{rpc-mode-CN5HBSB3.js → rpc-mode-I7CRGV4X.js} +1 -1
  16. package/dist/bundle/chunks/{schedule-command-I3IYHTWW.js → schedule-command-6LF6KHSF.js} +1 -1
  17. package/dist/bundle/chunks/{session-control-endpoint-W3EL4JYJ.js → session-control-endpoint-VV6PUFSF.js} +1 -1
  18. package/dist/bundle/chunks/{session-picker-HES2YE6K.js → session-picker-QZZ3DJER.js} +1 -1
  19. package/dist/bundle/chunks/session-worker.js +143 -143
  20. package/dist/bundle/cli.js +2 -2
  21. package/dist/bundle/index.js +1 -1
  22. package/dist/bundle/rpc-entry.js +1 -1
  23. package/dist/bundle/runtime-manifest.json +1 -1
  24. package/dist/core/lifecycle-ledger.d.ts +3 -0
  25. package/dist/core/lifecycle-ledger.js +18 -1
  26. package/dist/core/lifecycle-session-binding.js +1 -0
  27. package/dist/core/session-holders.js +2 -1
  28. package/dist/core/session-lifecycle.d.ts +3 -0
  29. package/dist/core/session-lifecycle.js +11 -1
  30. package/dist/core/session-move/export.d.ts +3 -3
  31. package/dist/core/session-move/export.js +32 -27
  32. package/dist/core/session-move/extension-state.d.ts +5 -5
  33. package/dist/core/session-move/extension-state.js +25 -25
  34. package/dist/core/session-move/finish.d.ts +1 -1
  35. package/dist/core/session-move/finish.js +14 -13
  36. package/dist/core/session-move/import.js +37 -36
  37. package/dist/core/session-move/move-ledger.d.ts +3 -3
  38. package/dist/core/session-move/move-ledger.js +7 -6
  39. package/dist/core/session-move/requeue.js +1 -1
  40. package/dist/core/session-move/revoke.d.ts +1 -1
  41. package/dist/core/session-move/revoke.js +5 -5
  42. package/dist/core/session-move/session-lock.d.ts +4 -3
  43. package/dist/core/session-move/session-lock.js +23 -9
  44. package/dist/core/session-rebind.d.ts +1 -1
  45. package/dist/core/session-rebind.js +61 -31
  46. package/dist/modes/rpc/session-move-commands.js +16 -15
  47. package/dist/modes/rpc/session-release.js +3 -3
  48. package/docs/release-guide.md +1 -1
  49. package/package.json +6 -6
@@ -1,8 +1,8 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
3
- import { writeFile } from "node:fs/promises";
2
+ import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
4
3
  import { basename, dirname, join } from "node:path";
5
- import { loadEntriesFromFile } from "../session-manager.js";
4
+ import { loadEntriesFromFileAsync } from "../session-manager.js";
5
+ import { pathExists } from "../tools/path-utils.js";
6
6
  import { isPlainId, parseMoveId } from "./bundle.js";
7
7
  import { SESSION_MOVED_IN, SESSION_RELEASED } from "./ownership.js";
8
8
  /**
@@ -45,36 +45,36 @@ function sealedAttempt(entries, moveId) {
45
45
  return isPlainId(attempt) ? attempt : undefined;
46
46
  }
47
47
  /** The folder of the release attempt that sealed the session for `moveId`, if that release staged one. */
48
- export function sealedOutgoingStateDir(sessionFile, moveId) {
49
- const attempt = sealedAttempt(loadEntriesFromFile(sessionFile), moveId);
48
+ export async function sealedOutgoingStateDir(sessionFile, moveId) {
49
+ const attempt = sealedAttempt(await loadEntriesFromFileAsync(sessionFile), moveId);
50
50
  return attempt === undefined ? undefined : join(outgoingStateDir(sessionFile, moveId), attempt);
51
51
  }
52
52
  /**
53
53
  * Creates a fresh, empty folder for one release attempt. Earlier attempts of the move that never sealed the session
54
54
  * are dead and removed (a handler still running for one of them may recreate its own folder, which is never read).
55
55
  */
56
- export function prepareOutgoingState(sessionFile, moveId) {
56
+ export async function prepareOutgoingState(sessionFile, moveId) {
57
57
  const root = outgoingStateDir(sessionFile, moveId);
58
- const sealed = sealedAttempt(loadEntriesFromFile(sessionFile), moveId);
59
- if (existsSync(root)) {
60
- for (const name of readdirSync(root)) {
58
+ const sealed = sealedAttempt(await loadEntriesFromFileAsync(sessionFile), moveId);
59
+ if (await pathExists(root)) {
60
+ for (const name of await readdir(root)) {
61
61
  if (name !== sealed)
62
- rmSync(join(root, name), { recursive: true, force: true });
62
+ await rm(join(root, name), { recursive: true, force: true });
63
63
  }
64
64
  }
65
65
  const attempt = newAttempt();
66
66
  const dir = join(root, attempt);
67
- mkdirSync(dir, { recursive: true });
67
+ await mkdir(dir, { recursive: true });
68
68
  return { attempt, dir };
69
69
  }
70
70
  /**
71
71
  * The state the last move into this session left for its extensions, if it has not been handed over yet. Only the
72
72
  * newest `session_moved_in` counts, and only until a `session_move_state_restored` for that move is written.
73
73
  */
74
- export function pendingMovedState(sessionFile) {
75
- if (!existsSync(sessionFile))
74
+ export async function pendingMovedState(sessionFile) {
75
+ if (!(await pathExists(sessionFile)))
76
76
  return undefined;
77
- const entries = loadEntriesFromFile(sessionFile);
77
+ const entries = await loadEntriesFromFileAsync(sessionFile);
78
78
  const movedIn = entries.findLast((entry) => entry.type === "custom" && entry.customType === SESSION_MOVED_IN);
79
79
  const moveId = movedIn === undefined ? undefined : moveIdOf(movedIn);
80
80
  if (moveId === undefined)
@@ -84,7 +84,7 @@ export function pendingMovedState(sessionFile) {
84
84
  return undefined;
85
85
  const id = parseMoveId(moveId);
86
86
  const dir = movedStateDir(sessionFile, id);
87
- return existsSync(dir) ? { moveId: id, dir } : undefined;
87
+ return (await pathExists(dir)) ? { moveId: id, dir } : undefined;
88
88
  }
89
89
  /** Delivery bookkeeping for one pending folder: the `delivered` marker and each delivery's own copy. */
90
90
  const deliveryDir = (pendingDir) => join(dirname(dirname(pendingDir)), "delivery", basename(pendingDir));
@@ -99,28 +99,28 @@ export async function restoreMovedState(sessionManager, deliver) {
99
99
  const sessionFile = sessionManager.getSessionFile();
100
100
  if (sessionFile === undefined)
101
101
  return;
102
- const pending = pendingMovedState(sessionFile);
102
+ const pending = await pendingMovedState(sessionFile);
103
103
  if (pending === undefined)
104
104
  return;
105
105
  const bookkeeping = deliveryDir(pending.dir);
106
106
  const marker = join(bookkeeping, DELIVERED_MARKER);
107
- const redelivered = existsSync(marker);
108
- if (existsSync(bookkeeping)) {
109
- for (const name of readdirSync(bookkeeping)) {
107
+ const redelivered = await pathExists(marker);
108
+ if (await pathExists(bookkeeping)) {
109
+ for (const name of await readdir(bookkeeping)) {
110
110
  if (name.startsWith(DELIVERY_COPY))
111
- rmSync(join(bookkeeping, name), { recursive: true, force: true });
111
+ await rm(join(bookkeeping, name), { recursive: true, force: true });
112
112
  }
113
113
  }
114
- mkdirSync(bookkeeping, { recursive: true });
114
+ await mkdir(bookkeeping, { recursive: true });
115
115
  await writeFile(marker, "");
116
116
  const copy = join(bookkeeping, `${DELIVERY_COPY}${newAttempt()}`);
117
- cpSync(pending.dir, copy, { recursive: true });
117
+ await cp(pending.dir, copy, { recursive: true });
118
118
  let restored;
119
119
  try {
120
120
  restored = await deliver({ moveId: pending.moveId, dir: copy, redelivered });
121
121
  }
122
122
  finally {
123
- rmSync(copy, { recursive: true, force: true });
123
+ await rm(copy, { recursive: true, force: true });
124
124
  }
125
125
  if (!restored)
126
126
  return;
@@ -128,7 +128,7 @@ export async function restoreMovedState(sessionManager, deliver) {
128
128
  move_id: pending.moveId,
129
129
  restored_at: new Date().toISOString(),
130
130
  });
131
- rmSync(pending.dir, { recursive: true, force: true });
132
- rmSync(bookkeeping, { recursive: true, force: true });
131
+ await rm(pending.dir, { recursive: true, force: true });
132
+ await rm(bookkeeping, { recursive: true, force: true });
133
133
  }
134
134
  //# sourceMappingURL=extension-state.js.map
@@ -9,7 +9,7 @@ export interface FinishMoveOptions {
9
9
  /** The receiver's signed answer. `receiving` changes nothing: the move is still open. */
10
10
  readonly answer: MoveAnswer;
11
11
  /** Runs inside the move lock before anything is read; throw to refuse. */
12
- readonly guard?: () => void;
12
+ readonly guard?: () => void | Promise<void>;
13
13
  }
14
14
  export declare class MoveMismatchError extends Error {
15
15
  readonly code = "move_mismatch";
@@ -1,5 +1,6 @@
1
- import { existsSync, rmSync } from "node:fs";
2
- import { loadEntriesFromFile, SessionManager } from "../session-manager.js";
1
+ import { rm } from "node:fs/promises";
2
+ import { loadEntriesFromFileAsync, SessionManager } from "../session-manager.js";
3
+ import { pathExists } from "../tools/path-utils.js";
3
4
  import { readManifest } from "./bundle.js";
4
5
  import { outgoingStateDir } from "./extension-state.js";
5
6
  import { moveStep } from "./move-steps.js";
@@ -32,17 +33,17 @@ export function finishMove(options) {
32
33
  }
33
34
  async function settleMove(options) {
34
35
  const { sessionFile, bundleDir, moveId, answer } = options;
35
- if (existsSync(bundleDir)) {
36
+ if (await pathExists(bundleDir)) {
36
37
  const bundled = (await readManifest(bundleDir)).moveId;
37
38
  if (bundled !== moveId)
38
39
  throw new MoveMismatchError(moveId, bundled, "the kept bundle");
39
40
  }
40
- const entries = loadEntriesFromFile(sessionFile);
41
+ const entries = await loadEntriesFromFileAsync(sessionFile);
41
42
  const ownership = sessionOwnership(entries);
42
43
  if (ownership.writable) {
43
44
  if (reclaimed(entries, moveId))
44
45
  await requeueDroppedInput(sessionFile, bundleDir);
45
- discardMoveFiles(sessionFile, bundleDir, moveId);
46
+ await discardMoveFiles(sessionFile, bundleDir, moveId);
46
47
  return;
47
48
  }
48
49
  if (ownership.moveId !== moveId)
@@ -50,7 +51,7 @@ async function settleMove(options) {
50
51
  if (ownership.state === "moved") {
51
52
  if ("revoked" in answer)
52
53
  throw new MoveAlreadyMovedError(moveId);
53
- discardMoveFiles(sessionFile, bundleDir, moveId);
54
+ await discardMoveFiles(sessionFile, bundleDir, moveId);
54
55
  return;
55
56
  }
56
57
  const sessionId = headerSessionId(entries) ?? "";
@@ -62,7 +63,7 @@ async function settleMove(options) {
62
63
  await takeBack(sessionFile, bundleDir, { move_id: moveId, session_id: sessionId, reason: "revoked" });
63
64
  break;
64
65
  }
65
- SessionManager.open(sessionFile).appendCustomEntry(SESSION_MOVED, {
66
+ (await SessionManager.openAsync(sessionFile)).appendCustomEntry(SESSION_MOVED, {
66
67
  move_id: moveId,
67
68
  session_id: sessionId,
68
69
  target: ownership.target,
@@ -85,20 +86,20 @@ async function settleMove(options) {
85
86
  }
86
87
  }
87
88
  moveStep("finish:requeued");
88
- discardMoveFiles(sessionFile, bundleDir, moveId);
89
+ await discardMoveFiles(sessionFile, bundleDir, moveId);
89
90
  }
90
91
  /** `session_reclaimed` makes the file writable again, then the queued input comes back under ids stable per move. */
91
92
  async function takeBack(sessionFile, bundleDir, data) {
92
- SessionManager.open(sessionFile).appendCustomEntry(SESSION_RECLAIMED, {
93
+ (await SessionManager.openAsync(sessionFile)).appendCustomEntry(SESSION_RECLAIMED, {
93
94
  ...data,
94
95
  reclaimed_at: new Date().toISOString(),
95
96
  });
96
97
  moveStep("finish:recorded");
97
98
  await requeueDroppedInput(sessionFile, bundleDir);
98
99
  }
99
- function discardMoveFiles(sessionFile, bundleDir, moveId) {
100
- rmSync(bundleDir, { recursive: true, force: true });
101
- rmSync(outgoingStateDir(sessionFile, moveId), { recursive: true, force: true });
100
+ async function discardMoveFiles(sessionFile, bundleDir, moveId) {
101
+ await rm(bundleDir, { recursive: true, force: true });
102
+ await rm(outgoingStateDir(sessionFile, moveId), { recursive: true, force: true });
102
103
  }
103
104
  function reclaimed(entries, moveId) {
104
105
  return entries.some((entry) => entry.type === "custom" &&
@@ -106,7 +107,7 @@ function reclaimed(entries, moveId) {
106
107
  entry.data?.move_id === moveId);
107
108
  }
108
109
  export async function requeueDroppedInput(sessionFile, bundleDir) {
109
- if (!existsSync(bundleDir))
110
+ if (!(await pathExists(bundleDir)))
110
111
  return;
111
112
  const manifest = await readManifest(bundleDir);
112
113
  await requeueInput(sessionFile, manifest.moveId, manifest.droppedUserMessages);
@@ -1,10 +1,11 @@
1
- import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
1
+ import { copyFile, mkdir, readdir, rm } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { normalizePath, resolvePath } from "../../utils/paths.js";
4
4
  import { compareRepositoryIdentities, readRepositoryIdentity, } from "../repository-identity.js";
5
- import { getDefaultSessionDir, loadEntriesFromFile, SessionManager } from "../session-manager.js";
5
+ import { getDefaultSessionDir, loadEntriesFromFileAsync, SessionManager } from "../session-manager.js";
6
6
  import { rebindSessionFile } from "../session-rebind.js";
7
7
  import { encodedSessionId, serializeByKey } from "../session-sidecar-store.js";
8
+ import { pathExists } from "../tools/path-utils.js";
8
9
  import { EXTENSION_STATE_PREFIX, MoveBundleInvalidError, readManifest, sha256File, verifyBundle, } from "./bundle.js";
9
10
  import { EXTENSION_STATE_PAYLOAD, movedStateDir } from "./extension-state.js";
10
11
  import { MoveFencedError } from "./move-ledger.js";
@@ -27,13 +28,13 @@ export class MoveTargetOccupiedError extends Error {
27
28
  this.name = "MoveTargetOccupiedError";
28
29
  }
29
30
  }
30
- const movedInFor = (file, moveId) => loadEntriesFromFile(file).some((entry) => entry.type === "custom" &&
31
+ const movedInFor = async (file, moveId) => (await loadEntriesFromFileAsync(file)).some((entry) => entry.type === "custom" &&
31
32
  entry.customType === SESSION_MOVED_IN &&
32
33
  entry.data?.move_id === moveId);
33
34
  /** What the session file records for the move: nothing, its import, or its import and then a revoke. */
34
- function importOutcome(file, moveId) {
35
+ async function importOutcome(file, moveId) {
35
36
  let outcome = "absent";
36
- for (const entry of loadEntriesFromFile(file)) {
37
+ for (const entry of await loadEntriesFromFileAsync(file)) {
37
38
  if (entry.type !== "custom")
38
39
  continue;
39
40
  const data = entry.data;
@@ -59,11 +60,11 @@ export class StatusPathMismatchError extends Error {
59
60
  * one revoked), so a file that is not this move's import is refused. A path with no file answers from the ledger.
60
61
  */
61
62
  export async function answerMoveStatus(ledger, moveId, sessionFile) {
62
- const landed = (target) => (existsSync(target) ? importOutcome(target, moveId) : "absent");
63
- if (sessionFile === undefined || !existsSync(sessionFile))
63
+ const landed = async (target) => ((await pathExists(target)) ? importOutcome(target, moveId) : "absent");
64
+ if (sessionFile === undefined || !(await pathExists(sessionFile)))
64
65
  return ledger.answerStatus(moveId, { movedIn: false, landed });
65
- return serializeByKey(moveLockKey(sessionFile), async () => {
66
- const outcome = importOutcome(sessionFile, moveId);
66
+ return serializeByKey(await moveLockKey(sessionFile), async () => {
67
+ const outcome = await importOutcome(sessionFile, moveId);
67
68
  if (outcome === "absent")
68
69
  throw new StatusPathMismatchError(moveId, sessionFile);
69
70
  return ledger.answerStatus(moveId, { movedIn: true, revoked: outcome === "revoked", landed });
@@ -83,10 +84,10 @@ export async function importMovedSession(options) {
83
84
  const target = join(sessionDir, manifest.sessionFileName);
84
85
  // Once this move's file landed, the move records no failure: only its `accepted` write can have been lost, so it stays
85
86
  // receiving and a re-run accepts it. `fail` asks this under the move's lock, where an overlapping commit cannot race it.
86
- const landed = () => existsSync(target) && movedInFor(target, manifest.moveId);
87
- if (landed()) {
87
+ const landed = async () => (await pathExists(target)) && (await movedInFor(target, manifest.moveId));
88
+ if (await landed()) {
88
89
  // The rename already committed; a crash or a failed write may have lost `accepted`, and the file is the authority.
89
- await options.ledger.confirmLanded(manifest.moveId, target, importOutcome(target, manifest.moveId) === "revoked");
90
+ await options.ledger.confirmLanded(manifest.moveId, target, (await importOutcome(target, manifest.moveId)) === "revoked");
90
91
  return { sessionFile: target };
91
92
  }
92
93
  try {
@@ -99,7 +100,7 @@ export async function importMovedSession(options) {
99
100
  }
100
101
  const read = options.readIdentity ?? readRepositoryIdentity;
101
102
  if (compareRepositoryIdentities(manifest.repository, await read(targetCwd)) !== "same") {
102
- if (!landed())
103
+ if (!(await landed()))
103
104
  await options.ledger.decline(manifest.moveId, "repository");
104
105
  throw new MoveRefusedError(targetCwd, manifest.sourceCwd);
105
106
  }
@@ -108,13 +109,13 @@ export async function importMovedSession(options) {
108
109
  const staging = join(sessionDir, ".move-staging", encodeURIComponent(manifest.moveId));
109
110
  try {
110
111
  const staged = await stage(options.bundleDir, manifest, staging, options.from ?? "");
111
- const sessionFile = await options.ledger.commit(manifest.moveId, target, () => rebindSessionFile(staged, targetCwd, sessionDir), () => {
112
- replacePredecessor(target, manifest.sessionId, sessionDir);
112
+ const sessionFile = await options.ledger.commit(manifest.moveId, target, () => rebindSessionFile(staged, targetCwd, sessionDir), async () => {
113
+ await replacePredecessor(target, manifest.sessionId, sessionDir);
113
114
  // Before the rename: a crash here re-copies on retry, and after it the state is already in place.
114
- placeExtensionState(staging, manifest, movedStateDir(target, manifest.moveId));
115
+ await placeExtensionState(staging, manifest, movedStateDir(target, manifest.moveId));
115
116
  });
116
117
  moveStep("import:accepted");
117
- rmSync(staging, { recursive: true, force: true });
118
+ await rm(staging, { recursive: true, force: true });
118
119
  return { sessionFile };
119
120
  }
120
121
  catch (error) {
@@ -128,22 +129,22 @@ export async function importMovedSession(options) {
128
129
  * It is removed before the rename: if the rename then fails, the move is recorded `import_failed`, the sender takes
129
130
  * the session back, and nothing writable was lost here (the removed copy could not be written).
130
131
  */
131
- function replacePredecessor(target, sessionId, sessionDir) {
132
- if (!existsSync(target))
132
+ async function replacePredecessor(target, sessionId, sessionDir) {
133
+ if (!(await pathExists(target)))
133
134
  return;
134
- const entries = loadEntriesFromFile(target);
135
+ const entries = await loadEntriesFromFileAsync(target);
135
136
  if (headerSessionId(entries) !== sessionId || sessionOwnership(entries).writable) {
136
137
  throw new MoveTargetOccupiedError(target);
137
138
  }
138
139
  const extensions = join(sessionDir, "extensions");
139
- if (existsSync(extensions)) {
140
- for (const extension of readdirSync(extensions, { withFileTypes: true })) {
140
+ if (await pathExists(extensions)) {
141
+ for (const extension of await readdir(extensions, { withFileTypes: true })) {
141
142
  if (extension.isDirectory())
142
- rmSync(join(extensions, extension.name, `${encodedSessionId(sessionId)}.json`), { force: true });
143
+ await rm(join(extensions, extension.name, `${encodedSessionId(sessionId)}.json`), { force: true });
143
144
  }
144
145
  }
145
- rmSync(join(sessionDir, "resident-blobs", sessionId), { recursive: true, force: true });
146
- rmSync(target, { force: true });
146
+ await rm(join(sessionDir, "resident-blobs", sessionId), { recursive: true, force: true });
147
+ await rm(target, { force: true });
147
148
  }
148
149
  /** Payload files are staged here, next to the staged session file, and placed from this checked copy. */
149
150
  const STAGED_PAYLOADS = ".payloads";
@@ -152,34 +153,34 @@ const extensionStateFiles = (manifest) => manifest.payloads.filter((payload) =>
152
153
  async function stagePayloads(bundleDir, manifest, staging) {
153
154
  for (const file of manifest.payloads.flatMap((payload) => payload.files)) {
154
155
  const to = join(staging, STAGED_PAYLOADS, file.path);
155
- mkdirSync(dirname(to), { recursive: true });
156
- copyFileSync(join(bundleDir, file.path), to);
156
+ await mkdir(dirname(to), { recursive: true });
157
+ await copyFile(join(bundleDir, file.path), to);
157
158
  if ((await sha256File(to)) !== file.sha256)
158
159
  throw new MoveBundleInvalidError(`${file.path} changed in transit`);
159
160
  }
160
161
  }
161
- function placeExtensionState(staging, manifest, dir) {
162
+ async function placeExtensionState(staging, manifest, dir) {
162
163
  const files = extensionStateFiles(manifest);
163
164
  if (files.length === 0)
164
165
  return;
165
- rmSync(dir, { recursive: true, force: true });
166
+ await rm(dir, { recursive: true, force: true });
166
167
  for (const file of files) {
167
168
  const to = join(dir, file.path.slice(EXTENSION_STATE_PREFIX.length));
168
- mkdirSync(dirname(to), { recursive: true });
169
- copyFileSync(join(staging, STAGED_PAYLOADS, file.path), to);
169
+ await mkdir(dirname(to), { recursive: true });
170
+ await copyFile(join(staging, STAGED_PAYLOADS, file.path), to);
170
171
  }
171
172
  }
172
173
  async function stage(bundleDir, manifest, staging, from) {
173
174
  const file = join(staging, manifest.sessionFileName);
174
- if (!(existsSync(file) && movedInFor(file, manifest.moveId))) {
175
- rmSync(staging, { recursive: true, force: true });
175
+ if (!((await pathExists(file)) && (await movedInFor(file, manifest.moveId)))) {
176
+ await rm(staging, { recursive: true, force: true });
176
177
  for (const item of [manifest.session, ...manifest.sidecars]) {
177
178
  const to = item === manifest.session ? file : join(staging, item.path.replace(/^files\//, ""));
178
- mkdirSync(dirname(to), { recursive: true });
179
- copyFileSync(join(bundleDir, item.path), to);
179
+ await mkdir(dirname(to), { recursive: true });
180
+ await copyFile(join(bundleDir, item.path), to);
180
181
  }
181
182
  await stagePayloads(bundleDir, manifest, staging);
182
- SessionManager.open(file, staging).appendCustomEntry(SESSION_MOVED_IN, {
183
+ (await SessionManager.openAsync(file, staging)).appendCustomEntry(SESSION_MOVED_IN, {
183
184
  move_id: manifest.moveId,
184
185
  session_id: manifest.sessionId,
185
186
  from,
@@ -73,7 +73,7 @@ export declare class MoveLedger {
73
73
  answerStatus(moveId: MoveId, here: {
74
74
  readonly movedIn: boolean;
75
75
  readonly revoked?: boolean;
76
- readonly landed?: (target: string) => LandingOutcome;
76
+ readonly landed?: (target: string) => LandingOutcome | Promise<LandingOutcome>;
77
77
  }): Promise<MoveAnswer>;
78
78
  /** Cancel, expiry or a refused chunk: only before the bundle was verified and receiving began, never after. */
79
79
  decline(moveId: MoveId, reason: DeclineReason): Promise<MoveRecord>;
@@ -83,7 +83,7 @@ export declare class MoveLedger {
83
83
  * commit may land it while this call waits for the lock), nothing is recorded and the current record is returned.
84
84
  */
85
85
  fail(moveId: MoveId, reason: FailReason, options?: {
86
- readonly unlessLanded?: () => boolean;
86
+ readonly unlessLanded?: () => boolean | Promise<boolean>;
87
87
  }): Promise<MoveRecord | undefined>;
88
88
  /**
89
89
  * Runs the import's commit (the atomic rename into `target`) under the move's lock. The record is stamped
@@ -91,7 +91,7 @@ export declare class MoveLedger {
91
91
  * file at the target then decides (`confirmLanded`, `reconcile`). Only the recorded target may be renamed into.
92
92
  * `prepare` runs before the stamp: whatever it does or refuses lands nothing, so a refusal there still records failed.
93
93
  */
94
- commit<T>(moveId: MoveId, target: string, rename: () => T | Promise<T>, prepare?: () => void): Promise<T>;
94
+ commit<T>(moveId: MoveId, target: string, rename: () => T | Promise<T>, prepare?: () => void | Promise<void>): Promise<T>;
95
95
  /**
96
96
  * The import found this move's file already landed at `target`: that file proves the commit, so a record a crash
97
97
  * left `receiving`, committing or lost becomes accepted there, and revoked when the file is (`revoked`). A target
@@ -1,5 +1,4 @@
1
- import { mkdirSync } from "node:fs";
2
- import { readFile, rename as renameFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, rename as renameFile, writeFile } from "node:fs/promises";
3
2
  import { join } from "node:path";
4
3
  import { serializeByKey } from "../session-sidecar-store.js";
5
4
  import { parseMoveId } from "./bundle.js";
@@ -138,7 +137,9 @@ export class MoveLedger {
138
137
  * commit may land it while this call waits for the lock), nothing is recorded and the current record is returned.
139
138
  */
140
139
  fail(moveId, reason, options = {}) {
141
- return this.locked(moveId, () => options.unlessLanded?.() ? this.current(moveId) : this.settleLocked(moveId, { state: "failed", reason }, []));
140
+ return this.locked(moveId, async () => (await options.unlessLanded?.())
141
+ ? this.current(moveId)
142
+ : this.settleLocked(moveId, { state: "failed", reason }, []));
142
143
  }
143
144
  /**
144
145
  * Runs the import's commit (the atomic rename into `target`) under the move's lock. The record is stamped
@@ -152,7 +153,7 @@ export class MoveLedger {
152
153
  const landing = await this.landing(moveId);
153
154
  if (!landsAt(record, landing, target))
154
155
  throw new MoveFencedError(moveId, record, landedElsewhere(record, landing));
155
- prepare?.();
156
+ await prepare?.();
156
157
  await this.write(moveId, { state: "receiving" }, { target, committing: true });
157
158
  moveStep("import:stamped");
158
159
  const result = await rename();
@@ -188,7 +189,7 @@ export class MoveLedger {
188
189
  const { target, committing } = await this.landing(moveId);
189
190
  if (landed === undefined || target === undefined)
190
191
  return record;
191
- const next = settledLanding(record, committing === true, landed(target));
192
+ const next = settledLanding(record, committing === true, await landed(target));
192
193
  if (next === undefined)
193
194
  return record;
194
195
  await this.write(moveId, next, { target });
@@ -275,7 +276,7 @@ export class MoveLedger {
275
276
  locked(moveId, fn) {
276
277
  const file = this.path(moveId);
277
278
  return serializeByKey(file, async () => {
278
- mkdirSync(this.dir, { recursive: true });
279
+ await mkdir(this.dir, { recursive: true });
279
280
  return this.#locks.hold(file, fn);
280
281
  });
281
282
  }
@@ -8,7 +8,7 @@ import { SessionManager } from "../session-manager.js";
8
8
  export async function requeueInput(sessionFile, moveId, texts) {
9
9
  if (texts.length === 0)
10
10
  return;
11
- const sessionManager = SessionManager.open(sessionFile);
11
+ const sessionManager = await SessionManager.openAsync(sessionFile);
12
12
  let next = 0;
13
13
  const inputs = new SessionPendingInputs({ sessionManager, nextQueuedId: () => `move-${moveId}-${next++}` });
14
14
  for (const [index, text] of texts.entries()) {
@@ -7,7 +7,7 @@ export interface RevokeMoveOptions {
7
7
  /** This device's move record; its accepted record is marked revoked once the copy is sealed. */
8
8
  readonly ledger: MoveLedger;
9
9
  /** Runs inside the move lock before anything is read; throw to refuse. */
10
- readonly guard?: () => void;
10
+ readonly guard?: () => void | Promise<void>;
11
11
  }
12
12
  export declare class RevokeMismatchError extends Error {
13
13
  readonly code = "move_mismatch";
@@ -1,4 +1,4 @@
1
- import { loadEntriesFromFile, SessionManager } from "../session-manager.js";
1
+ import { loadEntriesFromFileAsync, SessionManager } from "../session-manager.js";
2
2
  import { moveStep } from "./move-steps.js";
3
3
  import { headerSessionId, ownershipStep, SESSION_MOVED, SESSION_MOVED_IN, } from "./ownership.js";
4
4
  import { withSessionMoveLock } from "./session-lock.js";
@@ -19,14 +19,14 @@ export const REVOKED = "revoked";
19
19
  */
20
20
  export function revokeImportedMove(options) {
21
21
  return withSessionMoveLock(options.sessionFile, async () => {
22
- seal(options.sessionFile, options.moveId);
22
+ await seal(options.sessionFile, options.moveId);
23
23
  moveStep("revoke:sealed");
24
24
  await options.ledger.markRevoked(options.moveId);
25
25
  moveStep("revoke:marked");
26
26
  }, options.guard);
27
27
  }
28
- function seal(sessionFile, moveId) {
29
- const entries = loadEntriesFromFile(sessionFile);
28
+ async function seal(sessionFile, moveId) {
29
+ const entries = await loadEntriesFromFileAsync(sessionFile);
30
30
  const sessionId = headerSessionId(entries);
31
31
  let ownership = { writable: true };
32
32
  let latest;
@@ -42,7 +42,7 @@ function seal(sessionFile, moveId) {
42
42
  return;
43
43
  if (!(ownership.writable && ofThisMove && latest?.customType === SESSION_MOVED_IN))
44
44
  throw new RevokeMismatchError(moveId);
45
- SessionManager.open(sessionFile).appendCustomEntry(SESSION_MOVED, {
45
+ (await SessionManager.openAsync(sessionFile)).appendCustomEntry(SESSION_MOVED, {
46
46
  move_id: moveId,
47
47
  session_id: sessionId ?? "",
48
48
  target: data(latest, "from") ?? "",
@@ -1,5 +1,6 @@
1
- export declare function realSessionPath(path: string): string;
2
- export declare const moveLockKey: (sessionFile: string) => string;
1
+ export declare function realSessionPath(path: string): Promise<string>;
2
+ export declare function orderedRealPath(path: string): Promise<string>;
3
+ export declare const moveLockKey: (sessionFile: string) => Promise<string>;
3
4
  /**
4
5
  * Runs the sender's steps of a move (export, finish) one at a time per session file, across processes too.
5
6
  *
@@ -11,5 +12,5 @@ export declare const moveLockKey: (sessionFile: string) => string;
11
12
  * - `guard` runs first inside the queue, so a caller's own check (for example "not open on this host") sees the
12
13
  * state the step will act on.
13
14
  */
14
- export declare function withSessionMoveLock<T>(sessionFile: string, step: () => Promise<T>, guard?: () => void): Promise<T>;
15
+ export declare function withSessionMoveLock<T>(sessionFile: string, step: () => Promise<T>, guard?: () => void | Promise<void>): Promise<T>;
15
16
  //# sourceMappingURL=session-lock.d.ts.map
@@ -1,20 +1,34 @@
1
- import { realpathSync } from "node:fs";
1
+ import { realpath } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { withSessionMoveLock as withHolderMoveLock } from "../session-holders.js";
4
- import { loadEntriesFromFile } from "../session-manager.js";
4
+ import { loadEntriesFromFileAsync } from "../session-manager.js";
5
5
  import { serializeByKey } from "../session-sidecar-store.js";
6
6
  import { headerSessionId } from "./ownership.js";
7
- export function realSessionPath(path) {
7
+ export async function realSessionPath(path) {
8
8
  const absolute = resolve(path);
9
9
  try {
10
- return realpathSync(absolute);
10
+ return await realpath(absolute);
11
11
  }
12
12
  catch {
13
13
  return absolute;
14
14
  }
15
15
  }
16
16
  const lockKey = (resolvedPath) => `session-move:${resolvedPath}`;
17
- export const moveLockKey = (sessionFile) => lockKey(realSessionPath(sessionFile));
17
+ // Resolutions of one path settle one after another, in call order, so two steps asked for in order enter the
18
+ // per-file queue in that order even though resolving the real path is asynchronous. Each path has its own chain, so
19
+ // one stuck realpath never delays another file's; a chain is dropped once it is idle. `realSessionPath` never rejects.
20
+ const pathChains = new Map();
21
+ export function orderedRealPath(path) {
22
+ const key = resolve(path);
23
+ const next = (pathChains.get(key) ?? Promise.resolve("")).then(() => realSessionPath(path));
24
+ pathChains.set(key, next);
25
+ void next.then(() => {
26
+ if (pathChains.get(key) === next)
27
+ pathChains.delete(key);
28
+ });
29
+ return next;
30
+ }
31
+ export const moveLockKey = async (sessionFile) => lockKey(await orderedRealPath(sessionFile));
18
32
  /**
19
33
  * Runs the sender's steps of a move (export, finish) one at a time per session file, across processes too.
20
34
  *
@@ -26,11 +40,11 @@ export const moveLockKey = (sessionFile) => lockKey(realSessionPath(sessionFile)
26
40
  * - `guard` runs first inside the queue, so a caller's own check (for example "not open on this host") sees the
27
41
  * state the step will act on.
28
42
  */
29
- export function withSessionMoveLock(sessionFile, step, guard) {
30
- const path = realSessionPath(sessionFile);
43
+ export async function withSessionMoveLock(sessionFile, step, guard) {
44
+ const path = await orderedRealPath(sessionFile);
31
45
  return serializeByKey(lockKey(path), async () => {
32
- guard?.();
33
- const sessionId = headerSessionId(loadEntriesFromFile(path));
46
+ await guard?.();
47
+ const sessionId = headerSessionId(await loadEntriesFromFileAsync(path));
34
48
  return sessionId === undefined ? step() : withHolderMoveLock(path, sessionId, step);
35
49
  });
36
50
  }
@@ -1,5 +1,5 @@
1
1
  import { type RepositoryIdentity, type RepositoryMatch } from "./repository-identity.ts";
2
- export declare function readRecordedRepositoryIdentity(sessionFile: string): RepositoryIdentity | undefined;
2
+ export declare function readRecordedRepositoryIdentity(sessionFile: string): Promise<RepositoryIdentity | undefined>;
3
3
  /**
4
4
  * Whether `cwd` is the git repository a session was recorded in. The session side prefers the live
5
5
  * checkout at its recorded cwd and falls back to the identity the session recorded, because a moved