@code-yeongyu/senpi 2026.10.18 → 2026.10.19

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 (190) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/bundle/chunks/{anthropic-messages-DDBWM4JZ.js → anthropic-messages-EMTBHJMM.js} +2 -2
  3. package/dist/bundle/chunks/{args-2MYPVFPH.js → args-LWR7VFKS.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-3UG5TQU7.js → azure-openai-responses-SNWKFDXK.js} +1 -1
  5. package/dist/bundle/chunks/chatgpt-subscription.js +1 -1
  6. package/dist/bundle/chunks/{chunk-SOF44GFZ.js → chunk-35M5MVUZ.js} +149 -145
  7. package/dist/bundle/chunks/chunk-3DNYAO3P.js +2 -0
  8. package/dist/bundle/chunks/{chunk-T2MHSEQ2.js → chunk-5AOKDC6G.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-MYXNQ7YZ.js → chunk-7QPFFAJT.js} +3 -3
  10. package/dist/bundle/chunks/{chunk-R36U2DJC.js → chunk-AUZXSLZN.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-PMTG4YNS.js → chunk-BOCN744P.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-XTAWER7O.js → chunk-CICJOV6I.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-4UCLDXEF.js → chunk-FM7NDZR5.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-TBRWPNA4.js → chunk-FSENE7JK.js} +2 -2
  15. package/dist/bundle/chunks/{chunk-TUD7XRIS.js → chunk-G7LLAQXI.js} +3 -3
  16. package/dist/bundle/chunks/{chunk-BGD3MZ47.js → chunk-GOKAWZUO.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-XC6UG5SI.js → chunk-IM6ZWYTW.js} +2 -2
  18. package/dist/bundle/chunks/{chunk-KHATAB7P.js → chunk-KM4FMQII.js} +1 -1
  19. package/dist/bundle/chunks/chunk-LAWS3FYQ.js +11 -0
  20. package/dist/bundle/chunks/{chunk-W6YQYGE2.js → chunk-MVNVDGYF.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-W4BO6S7F.js → chunk-PMGEQYEP.js} +2 -2
  22. package/dist/bundle/chunks/chunk-QGJLZ6BI.js +2 -0
  23. package/dist/bundle/chunks/{chunk-5RQNCWS2.js → chunk-QJ6MUFXY.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-57Q7Q627.js → chunk-QRGAMVEE.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-W2257FYP.js → chunk-QXEWAKXA.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-UUMKAA7S.js → chunk-RL5QMGQD.js} +9 -9
  27. package/dist/bundle/chunks/{chunk-D2UQYZVK.js → chunk-SHKXJWK3.js} +1 -1
  28. package/dist/bundle/chunks/chunk-T36LJ6MW.js +3 -0
  29. package/dist/bundle/chunks/{chunk-S4V72F76.js → chunk-VCD7FD64.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-MYPTZCPF.js → chunk-VEKNDMUA.js} +1 -1
  31. package/dist/bundle/chunks/{chunk-IRBAGET4.js → chunk-VPV7ADF3.js} +4 -4
  32. package/dist/bundle/chunks/{chunk-6DDAEDY7.js → chunk-WEHIEACU.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-C2HSPFXN.js → chunk-WQLMQGOW.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-QZIKKUUP.js → chunk-X6TWPBI6.js} +2 -2
  35. package/dist/bundle/chunks/{chunk-VYWODOAM.js → chunk-YUMRZIKJ.js} +6 -6
  36. package/dist/bundle/chunks/chunk-ZPSJQHDK.js +395 -0
  37. package/dist/bundle/chunks/{cli-main-MVVPTGMS.js → cli-main-EABMHWFV.js} +1 -1
  38. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-H4ZPUBX4.js → cloudflare-workers-ai-system-one-PF7FU63D.js} +1 -1
  39. package/dist/bundle/chunks/{google-generative-ai-YLAYNQLO.js → google-generative-ai-UFCHYLRE.js} +1 -1
  40. package/dist/bundle/chunks/{google-vertex-THFXM5R2.js → google-vertex-2FWDY7XX.js} +1 -1
  41. package/dist/bundle/chunks/{help-fast-path-H4ERPCR5.js → help-fast-path-BYWJN6YX.js} +1 -1
  42. package/dist/bundle/chunks/{host-command-2CMRIKC3.js → host-command-5T34JLBP.js} +1 -1
  43. package/dist/bundle/chunks/{host-lifecycle-6Y2EKT4U.js → host-lifecycle-GSRIQP6Z.js} +1 -1
  44. package/dist/bundle/chunks/host-lifecycle.js +5 -5
  45. package/dist/bundle/chunks/{host-session-command-MVHY4TKA.js → host-session-command-LF6JODPX.js} +1 -1
  46. package/dist/bundle/chunks/{interactive-mode-2ZQ43N73.js → interactive-mode-GMWHBIUC.js} +1 -1
  47. package/dist/bundle/chunks/{list-tips-AWDAUHF2.js → list-tips-VSWED3PT.js} +1 -1
  48. package/dist/bundle/chunks/{mistral-conversations-GUGTVRCL.js → mistral-conversations-QHZLDTSP.js} +1 -1
  49. package/dist/bundle/chunks/{multi-session-host-NO4PIOVG.js → multi-session-host-3J7STKGD.js} +6 -6
  50. package/dist/bundle/chunks/openai-codex-responses-7YOBPZ2N.js +10 -0
  51. package/dist/bundle/chunks/{openai-completions-I5N2UIEK.js → openai-completions-HSZPYXEA.js} +1 -1
  52. package/dist/bundle/chunks/{openai-responses-SGLUJVZW.js → openai-responses-7W2KF3W7.js} +1 -1
  53. package/dist/bundle/chunks/openrouter.js +1 -1
  54. package/dist/bundle/chunks/{package-manager-cli-HTZQXYNI.js → package-manager-cli-LWQZSWKL.js} +1 -1
  55. package/dist/bundle/chunks/radius.js +1 -1
  56. package/dist/bundle/chunks/{rotation-stream-TOOIK43C.js → rotation-stream-QRRWL73X.js} +1 -1
  57. package/dist/bundle/chunks/{rpc-mode-6ZWGPICX.js → rpc-mode-6LSWRKXR.js} +1 -1
  58. package/dist/bundle/chunks/{schedule-command-2YW4B655.js → schedule-command-7CXMW4WK.js} +1 -1
  59. package/dist/bundle/chunks/{session-control-endpoint-XC2LPLCD.js → session-control-endpoint-E265WAIQ.js} +1 -1
  60. package/dist/bundle/chunks/{session-picker-7W7GIRJT.js → session-picker-XRB3LP5W.js} +1 -1
  61. package/dist/bundle/chunks/session-worker.js +315 -310
  62. package/dist/bundle/chunks/{typesafe-system-one-VBWWTJTF.js → typesafe-system-one-YA75435G.js} +1 -1
  63. package/dist/bundle/cli.js +2 -2
  64. package/dist/bundle/index.js +1 -1
  65. package/dist/bundle/rpc-entry.js +1 -1
  66. package/dist/bundle/runtime-manifest.json +1 -1
  67. package/dist/core/agent-run-continuation.d.ts +24 -0
  68. package/dist/core/agent-run-continuation.js +57 -0
  69. package/dist/core/agent-session.d.ts +38 -1
  70. package/dist/core/agent-session.js +122 -13
  71. package/dist/core/extension-handler-stall-report.d.ts +11 -0
  72. package/dist/core/extension-handler-stall-report.js +26 -0
  73. package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +1 -0
  74. package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +13 -1
  75. package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
  76. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +2 -0
  77. package/dist/core/extensions/builtin/anthropic-subscription/executable.d.ts +5 -0
  78. package/dist/core/extensions/builtin/anthropic-subscription/executable.js +11 -1
  79. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
  80. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +3 -2
  81. package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -0
  82. package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +29 -0
  83. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +2 -0
  84. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +4 -0
  85. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +4 -2
  86. package/dist/core/extensions/handler-budget.d.ts +84 -0
  87. package/dist/core/extensions/handler-budget.js +136 -0
  88. package/dist/core/extensions/runner.d.ts +23 -0
  89. package/dist/core/extensions/runner.js +184 -99
  90. package/dist/core/footer-data-provider.js +2 -1
  91. package/dist/core/keybindings.d.ts +1 -1
  92. package/dist/core/keybindings.js +3 -2
  93. package/dist/core/model-resolver.js +1 -1
  94. package/dist/core/pending-input-journal.d.ts +6 -1
  95. package/dist/core/pending-input-journal.js +90 -34
  96. package/dist/core/pending-input-public.d.ts +11 -0
  97. package/dist/core/pending-input-public.js +10 -0
  98. package/dist/core/pending-input-session-port.d.ts +15 -0
  99. package/dist/core/pending-input-session-port.js +93 -0
  100. package/dist/core/pending-input-session.d.ts +108 -0
  101. package/dist/core/pending-input-session.js +220 -0
  102. package/dist/core/pending-input-store.d.ts +32 -1
  103. package/dist/core/pending-input-store.js +74 -26
  104. package/dist/core/pending-input-types.d.ts +25 -4
  105. package/dist/core/pending-input.js +36 -18
  106. package/dist/core/session-entry-file.js +1 -0
  107. package/dist/core/session-log.js +1 -1
  108. package/dist/core/session-manager.d.ts +1 -0
  109. package/dist/core/session-manager.js +5 -1
  110. package/dist/core/session-move/bundle.d.ts +39 -0
  111. package/dist/core/session-move/bundle.js +108 -0
  112. package/dist/core/session-move/export.d.ts +18 -0
  113. package/dist/core/session-move/export.js +83 -0
  114. package/dist/core/session-move/finish.d.ts +24 -0
  115. package/dist/core/session-move/finish.js +81 -0
  116. package/dist/core/session-move/import.d.ts +32 -0
  117. package/dist/core/session-move/import.js +124 -0
  118. package/dist/core/session-move/move-ledger.d.ts +47 -0
  119. package/dist/core/session-move/move-ledger.js +151 -0
  120. package/dist/core/session-move/ownership.d.ts +61 -0
  121. package/dist/core/session-move/ownership.js +89 -0
  122. package/dist/core/session-move/requeue.d.ts +8 -0
  123. package/dist/core/session-move/requeue.js +18 -0
  124. package/dist/core/session-move/write-guard.d.ts +22 -0
  125. package/dist/core/session-move/write-guard.js +44 -0
  126. package/dist/core/settings-manager.d.ts +13 -0
  127. package/dist/core/settings-manager.js +17 -0
  128. package/dist/core/turn-deadline.d.ts +2 -0
  129. package/dist/core/turn-deadline.js +10 -3
  130. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +7 -0
  131. package/dist/modes/interactive/components/ask-user-async-widget.js +20 -6
  132. package/dist/modes/rpc/connection-handler.js +3 -0
  133. package/dist/modes/rpc/effect-commands.js +3 -4
  134. package/dist/modes/rpc/host-decision.d.ts +1 -1
  135. package/dist/modes/rpc/host-decision.js +2 -0
  136. package/dist/modes/rpc/host-ensure-liveness.d.ts +8 -0
  137. package/dist/modes/rpc/host-ensure-liveness.js +18 -1
  138. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  139. package/dist/modes/rpc/host-ensure.js +8 -2
  140. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +9 -2
  141. package/dist/modes/rpc/host-lifecycle-proxy.js +36 -8
  142. package/dist/modes/rpc/host-observe-request.js +1 -0
  143. package/dist/modes/rpc/multi-session-host.js +3 -33
  144. package/dist/modes/rpc/rpc-client.d.ts +10 -0
  145. package/dist/modes/rpc/rpc-client.js +11 -0
  146. package/dist/modes/rpc/rpc-input-validation.js +8 -0
  147. package/dist/modes/rpc/rpc-types.d.ts +25 -1
  148. package/dist/modes/rpc/session-command-activity.js +1 -0
  149. package/dist/modes/rpc/session-control-actions-data.js +17 -8
  150. package/dist/modes/rpc/session-held.js +1 -0
  151. package/dist/modes/rpc/session-release.js +16 -1
  152. package/dist/modes/rpc/socket-ownership.d.ts +17 -0
  153. package/dist/modes/rpc/socket-ownership.js +52 -0
  154. package/dist/modes/rpc/tool-child/admission.d.ts +42 -0
  155. package/dist/modes/rpc/tool-child/admission.js +81 -0
  156. package/dist/modes/rpc/tool-child/admission.test.d.ts +2 -0
  157. package/dist/modes/rpc/tool-child/admission.test.js +166 -0
  158. package/dist/modes/rpc/tool-child/codec.d.ts +21 -0
  159. package/dist/modes/rpc/tool-child/codec.js +137 -0
  160. package/dist/modes/rpc/tool-child/codec.test.d.ts +2 -0
  161. package/dist/modes/rpc/tool-child/codec.test.js +234 -0
  162. package/dist/modes/rpc/tool-child/context.d.ts +42 -0
  163. package/dist/modes/rpc/tool-child/context.js +23 -0
  164. package/dist/modes/rpc/tool-child/flow-control.d.ts +24 -0
  165. package/dist/modes/rpc/tool-child/flow-control.js +80 -0
  166. package/dist/modes/rpc/tool-child/payload-ref-paths.d.ts +9 -0
  167. package/dist/modes/rpc/tool-child/payload-ref-paths.js +45 -0
  168. package/dist/modes/rpc/tool-child/payload-ref.d.ts +58 -0
  169. package/dist/modes/rpc/tool-child/payload-ref.js +166 -0
  170. package/dist/modes/rpc/tool-child/payload-ref.test.d.ts +2 -0
  171. package/dist/modes/rpc/tool-child/payload-ref.test.js +254 -0
  172. package/dist/modes/rpc/tool-child/payload-wire.d.ts +13 -0
  173. package/dist/modes/rpc/tool-child/payload-wire.js +22 -0
  174. package/dist/modes/rpc/tool-child/protocol-guard.d.ts +9 -0
  175. package/dist/modes/rpc/tool-child/protocol-guard.js +128 -0
  176. package/dist/modes/rpc/tool-child/protocol.d.ts +192 -0
  177. package/dist/modes/rpc/tool-child/protocol.js +54 -0
  178. package/dist/utils/wsl.d.ts +2 -2
  179. package/dist/utils/wsl.js +4 -4
  180. package/docs/providers.md +2 -0
  181. package/docs/rpc.md +20 -2
  182. package/docs/settings.md +1 -0
  183. package/docs/terminal-setup.md +2 -0
  184. package/package.json +10 -6
  185. package/dist/bundle/chunks/chunk-COJK5FDH.js +0 -2
  186. package/dist/bundle/chunks/chunk-ES3GHZE5.js +0 -395
  187. package/dist/bundle/chunks/chunk-IDXW4FAO.js +0 -2
  188. package/dist/bundle/chunks/chunk-PFNBY43W.js +0 -2
  189. package/dist/bundle/chunks/chunk-SA2ZLWBD.js +0 -11
  190. package/dist/bundle/chunks/openai-codex-responses-5LRKZCBW.js +0 -10
@@ -15,6 +15,7 @@ import { loadEntriesFromFile, loadEntriesFromFileAsync, parseSessionEntryLine, t
15
15
  import { materializeSessionEntries } from "./session-entry-materializer.js";
16
16
  import { replaceFileAtomically } from "./session-file-replace.js";
17
17
  import { planLoadedTail } from "./session-loaded-tail.js";
18
+ import { SessionWriteGuard } from "./session-move/write-guard.js";
18
19
  import { ResidentStringStore } from "./session-resident-store.js";
19
20
  import { discardFailedFirstFlush, discardFailedFirstFlushAsync, truncateToLastCompleteLine, } from "./session-write-recovery.js";
20
21
  import { hasOtherLiveSessionWriter, registerSessionWriter, reserveSessionWrite, unregisterSessionWriter, } from "./session-write-reservation.js";
@@ -677,6 +678,7 @@ function isConversationEntry(entry) {
677
678
  export class SessionManager {
678
679
  constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions, preloadedFileEntries) {
679
680
  this.sessionId = "";
681
+ this.writeGuard = new SessionWriteGuard();
680
682
  this.flushed = false;
681
683
  // Set when an append to the flushed file failed and may have left a partial last line.
682
684
  this.tailMayBeTorn = false;
@@ -874,7 +876,7 @@ export class SessionManager {
874
876
  // the JSONL is the authority for every entry and this store rewrites what it
875
877
  // evicts, so clearing them bounds the directory to one process lifetime.
876
878
  this._releaseBlobsDirUnlessShared();
877
- if (migrateToCurrentVersion(this.fileEntries)) {
879
+ if (migrateToCurrentVersion(this.fileEntries) && this.writeGuard.isWritable(this.fileEntries)) {
878
880
  this._rewriteFile();
879
881
  }
880
882
  return true;
@@ -1034,6 +1036,7 @@ export class SessionManager {
1034
1036
  _rewriteFile() {
1035
1037
  if (!this.persist || !this.sessionFile)
1036
1038
  return;
1039
+ this.writeGuard.assertRewritable(this.fileEntries, this.sessionFile);
1037
1040
  reserveSessionWrite(this.sessionFile);
1038
1041
  replaceFileAtomically(this.sessionFile, this._serializedFileEntries());
1039
1042
  }
@@ -1162,6 +1165,7 @@ export class SessionManager {
1162
1165
  _persist(entry) {
1163
1166
  if (!this.persist || !this.sessionFile)
1164
1167
  return;
1168
+ this.writeGuard.assertWritable(this.fileEntries, entry, this.sessionFile);
1165
1169
  reserveSessionWrite(this.sessionFile);
1166
1170
  const persistedEntry = this.residentStore.materialize(entry);
1167
1171
  if (this.flushed) {
@@ -0,0 +1,39 @@
1
+ import { type RepositoryIdentity } from "../repository-identity.ts";
2
+ import type { MoveId } from "./ownership.ts";
3
+ export declare const MOVE_BUNDLE_FORMAT = 1;
4
+ export declare const MANIFEST_FILE = "manifest.json";
5
+ export declare const SESSION_BUNDLE_PATH = "files/session.jsonl";
6
+ export interface BundleFile {
7
+ readonly path: string;
8
+ readonly sha256: string;
9
+ }
10
+ /**
11
+ * Optional extra payloads travel next to the session without changing its shape. Phase 1 defines none; a later
12
+ * whole-folder transfer adds its own kind here, and an importer that does not know a kind refuses the bundle.
13
+ */
14
+ export interface MovePayload {
15
+ readonly kind: string;
16
+ readonly files: readonly BundleFile[];
17
+ }
18
+ export interface MoveManifest {
19
+ readonly format: typeof MOVE_BUNDLE_FORMAT;
20
+ readonly moveId: MoveId;
21
+ readonly sessionId: string;
22
+ readonly sessionFileName: string;
23
+ readonly sourceCwd: string;
24
+ readonly repository: RepositoryIdentity | undefined;
25
+ readonly session: BundleFile;
26
+ readonly sidecars: readonly BundleFile[];
27
+ readonly droppedUserMessages: readonly string[];
28
+ readonly payloads: readonly MovePayload[];
29
+ }
30
+ export declare class MoveBundleInvalidError extends Error {
31
+ readonly code = "move_bundle_invalid";
32
+ constructor(detail: string);
33
+ }
34
+ export declare function sha256File(path: string): string;
35
+ export declare function parsePlainId(value: unknown, what: string): string;
36
+ export declare function parseMoveId(value: unknown): MoveId;
37
+ export declare function readManifest(bundleDir: string): MoveManifest;
38
+ export declare function verifyBundle(bundleDir: string, manifest: MoveManifest): void;
39
+ //# sourceMappingURL=bundle.d.ts.map
@@ -0,0 +1,108 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, posix, win32 } from "node:path";
4
+ import { parseRepositoryIdentity } from "../repository-identity.js";
5
+ export const MOVE_BUNDLE_FORMAT = 1;
6
+ export const MANIFEST_FILE = "manifest.json";
7
+ export const SESSION_BUNDLE_PATH = "files/session.jsonl";
8
+ export class MoveBundleInvalidError extends Error {
9
+ constructor(detail) {
10
+ super(`The moved session could not be verified (${detail}); it was not imported. The original device keeps it.`);
11
+ this.code = "move_bundle_invalid";
12
+ this.name = "MoveBundleInvalidError";
13
+ }
14
+ }
15
+ export function sha256File(path) {
16
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
17
+ }
18
+ const isString = (value) => typeof value === "string";
19
+ /** Both separators count: a bundle written on one OS is read on another. */
20
+ function leavesBundle(path) {
21
+ return posix.isAbsolute(path) || win32.isAbsolute(path) || path.split(/[\\/]/).includes("..");
22
+ }
23
+ function parseFile(value) {
24
+ const file = value;
25
+ if (typeof file !== "object" || file === null || !isString(file.path) || !isString(file.sha256))
26
+ throw new MoveBundleInvalidError("a listed file is malformed");
27
+ if (leavesBundle(file.path))
28
+ throw new MoveBundleInvalidError(`a listed path leaves the bundle: ${file.path}`);
29
+ return { path: file.path, sha256: file.sha256 };
30
+ }
31
+ /**
32
+ * Move ids and session ids become folder and file names (staging folder, ledger record, resident blobs), so they
33
+ * must be plain ids: a letter or digit first (never `.` or `..`), then letters, digits, `.`, `_`, `-`, at most 128.
34
+ */
35
+ const PLAIN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
36
+ export function parsePlainId(value, what) {
37
+ if (typeof value !== "string" || !PLAIN_ID.test(value))
38
+ throw new MoveBundleInvalidError(`the ${what} is not a plain id: ${JSON.stringify(value)}`);
39
+ return value;
40
+ }
41
+ export function parseMoveId(value) {
42
+ return parsePlainId(value, "move id");
43
+ }
44
+ /** The importer joins this name onto its session folder, so it must be a plain session file name and nothing else. */
45
+ const SESSION_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*\.jsonl$/;
46
+ function parseSessionFileName(value) {
47
+ if (!SESSION_FILE_NAME.test(value) || value.includes(".."))
48
+ throw new MoveBundleInvalidError(`the session file name is not a plain file name: ${value}`);
49
+ return value;
50
+ }
51
+ export function readManifest(bundleDir) {
52
+ let raw;
53
+ try {
54
+ raw = JSON.parse(readFileSync(join(bundleDir, MANIFEST_FILE), "utf8"));
55
+ }
56
+ catch (error) {
57
+ throw new MoveBundleInvalidError(`manifest unreadable: ${error instanceof Error ? error.message : String(error)}`);
58
+ }
59
+ const m = raw;
60
+ if (m.format !== MOVE_BUNDLE_FORMAT)
61
+ throw new MoveBundleInvalidError(`unknown bundle format ${String(m.format)}`);
62
+ for (const key of ["moveId", "sessionId", "sessionFileName", "sourceCwd"]) {
63
+ if (!isString(m[key]))
64
+ throw new MoveBundleInvalidError(`missing ${key}`);
65
+ }
66
+ const payloads = Array.isArray(m.payloads) ? m.payloads : [];
67
+ if (payloads.length > 0)
68
+ throw new MoveBundleInvalidError("it carries a payload this version cannot import");
69
+ const dropped = Array.isArray(m.droppedUserMessages) ? m.droppedUserMessages : [];
70
+ if (!dropped.every(isString))
71
+ throw new MoveBundleInvalidError("malformed queued input");
72
+ return {
73
+ format: MOVE_BUNDLE_FORMAT,
74
+ moveId: parseMoveId(m.moveId),
75
+ sessionId: parsePlainId(m.sessionId, "session id"),
76
+ sessionFileName: parseSessionFileName(m.sessionFileName),
77
+ sourceCwd: m.sourceCwd,
78
+ repository: parseRepositoryIdentity(m.repository),
79
+ session: parseFile(m.session),
80
+ sidecars: (Array.isArray(m.sidecars) ? m.sidecars : []).map(parseFile),
81
+ droppedUserMessages: dropped,
82
+ payloads: [],
83
+ };
84
+ }
85
+ export function verifyBundle(bundleDir, manifest) {
86
+ for (const file of [manifest.session, ...manifest.sidecars]) {
87
+ let actual;
88
+ try {
89
+ actual = sha256File(join(bundleDir, file.path));
90
+ }
91
+ catch {
92
+ throw new MoveBundleInvalidError(`missing ${file.path}`);
93
+ }
94
+ if (actual !== file.sha256)
95
+ throw new MoveBundleInvalidError(`${file.path} changed in transit`);
96
+ }
97
+ const header = readFileSync(join(bundleDir, manifest.session.path), "utf8").split("\n", 1)[0] ?? "";
98
+ let id;
99
+ try {
100
+ id = JSON.parse(header).id;
101
+ }
102
+ catch {
103
+ id = undefined;
104
+ }
105
+ if (id !== manifest.sessionId)
106
+ throw new MoveBundleInvalidError("the session file's own id does not match the bundle's session id");
107
+ }
108
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1,18 @@
1
+ import { type MoveManifest } from "./bundle.ts";
2
+ import { type MoveId } from "./ownership.ts";
3
+ export interface ExportSessionForMoveOptions {
4
+ /** A session no live runtime holds: an RPC host releases it first (`release_session` reason `move`). */
5
+ readonly sessionFile: string;
6
+ readonly moveId: MoveId;
7
+ readonly target: string;
8
+ /** Queued user input the release took out of the session (`dropped.user_messages`), in order. */
9
+ readonly droppedUserMessages: readonly string[];
10
+ readonly bundleDir: string;
11
+ }
12
+ /**
13
+ * Seals the session for the move (a `session_released` entry with reason `move`; from then on this device refuses
14
+ * writes) and writes a self-verifying bundle: the session file, its extension sidecars and resident blobs, and a
15
+ * manifest. Running it again for the same move rewrites the same bundle without a second release entry.
16
+ */
17
+ export declare function exportSessionForMove(options: ExportSessionForMoveOptions): Promise<MoveManifest>;
18
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1,83 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { SessionHeldError } from "../session-hold-errors.js";
4
+ import { liveSessionHolders } from "../session-holders.js";
5
+ import { loadEntriesFromFile, SessionManager } from "../session-manager.js";
6
+ import { readRecordedRepositoryIdentity, readSessionCwd } from "../session-rebind.js";
7
+ import { encodedSessionId } from "../session-sidecar-store.js";
8
+ import { MANIFEST_FILE, MOVE_BUNDLE_FORMAT, parseMoveId, SESSION_BUNDLE_PATH, sha256File, } from "./bundle.js";
9
+ import { headerSessionId, SESSION_RELEASED, SessionMovedAwayError, sessionOwnership, } from "./ownership.js";
10
+ /**
11
+ * Seals the session for the move (a `session_released` entry with reason `move`; from then on this device refuses
12
+ * writes) and writes a self-verifying bundle: the session file, its extension sidecars and resident blobs, and a
13
+ * manifest. Running it again for the same move rewrites the same bundle without a second release entry.
14
+ */
15
+ export async function exportSessionForMove(options) {
16
+ const { sessionFile } = options;
17
+ const moveId = parseMoveId(options.moveId);
18
+ const entries = loadEntriesFromFile(sessionFile);
19
+ const sessionId = headerSessionId(entries) ?? "";
20
+ // A live runtime could still write the transcript, its sidecars or its resident blobs after the seal.
21
+ const holders = await liveSessionHolders(sessionFile, sessionId);
22
+ if (holders.length > 0)
23
+ throw new SessionHeldError(sessionFile, holders);
24
+ const ownership = sessionOwnership(entries);
25
+ if (!ownership.writable && !(ownership.state === "moving" && ownership.moveId === moveId)) {
26
+ throw new SessionMovedAwayError(sessionFile, ownership);
27
+ }
28
+ if (ownership.writable) {
29
+ SessionManager.open(sessionFile).appendCustomEntry(SESSION_RELEASED, {
30
+ reason: "move",
31
+ move_id: moveId,
32
+ session_id: sessionId,
33
+ target: options.target,
34
+ released_at: new Date().toISOString(),
35
+ });
36
+ }
37
+ const staging = `${options.bundleDir}.partial-${process.pid}`;
38
+ rmSync(staging, { recursive: true, force: true });
39
+ mkdirSync(join(staging, dirname(SESSION_BUNDLE_PATH)), { recursive: true });
40
+ copyFileSync(sessionFile, join(staging, SESSION_BUNDLE_PATH));
41
+ const sidecars = copySidecars(dirname(sessionFile), sessionId, staging);
42
+ const manifest = {
43
+ format: MOVE_BUNDLE_FORMAT,
44
+ moveId,
45
+ sessionId,
46
+ sessionFileName: basename(sessionFile),
47
+ sourceCwd: readSessionCwd(sessionFile) ?? "",
48
+ repository: readRecordedRepositoryIdentity(sessionFile),
49
+ session: { path: SESSION_BUNDLE_PATH, sha256: sha256File(join(staging, SESSION_BUNDLE_PATH)) },
50
+ sidecars,
51
+ droppedUserMessages: [...options.droppedUserMessages],
52
+ payloads: [],
53
+ };
54
+ writeFileSync(join(staging, MANIFEST_FILE), `${JSON.stringify(manifest, null, 1)}\n`);
55
+ rmSync(options.bundleDir, { recursive: true, force: true });
56
+ renameSync(staging, options.bundleDir);
57
+ return manifest;
58
+ }
59
+ function copySidecars(sessionDir, sessionId, staging) {
60
+ const sources = [];
61
+ const extensions = join(sessionDir, "extensions");
62
+ const sidecarName = `${encodedSessionId(sessionId)}.json`;
63
+ if (existsSync(extensions)) {
64
+ for (const extension of readdirSync(extensions, { withFileTypes: true })) {
65
+ const file = join(extensions, extension.name, sidecarName);
66
+ if (extension.isDirectory() && existsSync(file))
67
+ sources.push(file);
68
+ }
69
+ }
70
+ sources.push(...filesUnder(join(sessionDir, "resident-blobs", sessionId)));
71
+ return sources.map((source) => {
72
+ const path = join("files", relative(sessionDir, source));
73
+ mkdirSync(dirname(join(staging, path)), { recursive: true });
74
+ copyFileSync(source, join(staging, path));
75
+ return { path, sha256: sha256File(join(staging, path)) };
76
+ });
77
+ }
78
+ function filesUnder(dir) {
79
+ if (!existsSync(dir))
80
+ return [];
81
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? filesUnder(join(dir, entry.name)) : [join(dir, entry.name)]);
82
+ }
83
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1,24 @@
1
+ import type { MoveStatus } from "./move-ledger.ts";
2
+ import { type MoveId } from "./ownership.ts";
3
+ export interface FinishMoveOptions {
4
+ readonly sessionFile: string;
5
+ /** The bundle this device kept while the move was open; it holds the input the release took out. */
6
+ readonly bundleDir: string;
7
+ /** The move the answer is about; the bundle and the file must be about the same move. */
8
+ readonly moveId: MoveId;
9
+ /** The receiver's signed answer. `receiving` changes nothing: the move is still open. */
10
+ readonly answer: MoveStatus;
11
+ }
12
+ export declare class MoveMismatchError extends Error {
13
+ readonly code = "move_mismatch";
14
+ constructor(expected: MoveId, found: string, what: string);
15
+ }
16
+ /**
17
+ * Closes a move on the sending device from the receiver's answer. `accepted` records `session_moved` (read-only for
18
+ * good). `declined`/`failed` takes the session back: `session_reclaimed` makes the file writable, the queued input
19
+ * is re-queued under ids stable per move, then the bundle goes. While the bundle exists the re-queue runs again on
20
+ * the next call, so a crash between any two steps loses nothing and doubles nothing.
21
+ */
22
+ export declare function finishMove(options: FinishMoveOptions): Promise<void>;
23
+ export declare function requeueDroppedInput(sessionFile: string, bundleDir: string): Promise<void>;
24
+ //# sourceMappingURL=finish.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { loadEntriesFromFile, SessionManager } from "../session-manager.js";
3
+ import { readManifest } from "./bundle.js";
4
+ import { headerSessionId, SESSION_MOVED, SESSION_RECLAIMED, sessionOwnership } from "./ownership.js";
5
+ import { requeueInput } from "./requeue.js";
6
+ export class MoveMismatchError extends Error {
7
+ constructor(expected, found, what) {
8
+ super(`The answer is about move ${expected}, but ${what} belongs to move ${found}; nothing was changed. Finish each move with its own answer and bundle.`);
9
+ this.code = "move_mismatch";
10
+ this.name = "MoveMismatchError";
11
+ }
12
+ }
13
+ /**
14
+ * Closes a move on the sending device from the receiver's answer. `accepted` records `session_moved` (read-only for
15
+ * good). `declined`/`failed` takes the session back: `session_reclaimed` makes the file writable, the queued input
16
+ * is re-queued under ids stable per move, then the bundle goes. While the bundle exists the re-queue runs again on
17
+ * the next call, so a crash between any two steps loses nothing and doubles nothing.
18
+ */
19
+ export async function finishMove(options) {
20
+ const { sessionFile, bundleDir, moveId, answer } = options;
21
+ if (existsSync(bundleDir)) {
22
+ const bundled = readManifest(bundleDir).moveId;
23
+ if (bundled !== moveId)
24
+ throw new MoveMismatchError(moveId, bundled, "the kept bundle");
25
+ }
26
+ const entries = loadEntriesFromFile(sessionFile);
27
+ const ownership = sessionOwnership(entries);
28
+ if (ownership.writable) {
29
+ if (reclaimed(entries, moveId))
30
+ await requeueDroppedInput(sessionFile, bundleDir);
31
+ rmSync(bundleDir, { recursive: true, force: true });
32
+ return;
33
+ }
34
+ if (ownership.moveId !== moveId)
35
+ throw new MoveMismatchError(moveId, ownership.moveId, "the session file");
36
+ if (ownership.state === "moved") {
37
+ rmSync(bundleDir, { recursive: true, force: true });
38
+ return;
39
+ }
40
+ const sessionId = headerSessionId(entries) ?? "";
41
+ switch (answer.state) {
42
+ case "receiving":
43
+ return;
44
+ case "accepted":
45
+ SessionManager.open(sessionFile).appendCustomEntry(SESSION_MOVED, {
46
+ move_id: moveId,
47
+ session_id: sessionId,
48
+ target: ownership.target,
49
+ moved_at: new Date().toISOString(),
50
+ });
51
+ break;
52
+ case "declined":
53
+ case "failed":
54
+ SessionManager.open(sessionFile).appendCustomEntry(SESSION_RECLAIMED, {
55
+ move_id: moveId,
56
+ session_id: sessionId,
57
+ reason: answer.state,
58
+ detail: answer.reason,
59
+ reclaimed_at: new Date().toISOString(),
60
+ });
61
+ await requeueDroppedInput(sessionFile, bundleDir);
62
+ break;
63
+ default: {
64
+ const unreachable = answer;
65
+ throw new Error(`Unknown move answer ${JSON.stringify(unreachable)}`);
66
+ }
67
+ }
68
+ rmSync(bundleDir, { recursive: true, force: true });
69
+ }
70
+ function reclaimed(entries, moveId) {
71
+ return entries.some((entry) => entry.type === "custom" &&
72
+ entry.customType === SESSION_RECLAIMED &&
73
+ entry.data?.move_id === moveId);
74
+ }
75
+ export async function requeueDroppedInput(sessionFile, bundleDir) {
76
+ if (!existsSync(bundleDir))
77
+ return;
78
+ const manifest = readManifest(bundleDir);
79
+ await requeueInput(sessionFile, manifest.moveId, manifest.droppedUserMessages);
80
+ }
81
+ //# sourceMappingURL=finish.js.map
@@ -0,0 +1,32 @@
1
+ import { type RepositoryIdentity } from "../repository-identity.ts";
2
+ import { type MoveLedger, type MoveStatus } from "./move-ledger.ts";
3
+ import { type MoveId } from "./ownership.ts";
4
+ export interface ImportMovedSessionOptions {
5
+ readonly bundleDir: string;
6
+ readonly targetCwd: string;
7
+ readonly ledger: MoveLedger;
8
+ readonly sessionDir?: string;
9
+ readonly from?: string;
10
+ readonly readIdentity?: (dir: string) => Promise<RepositoryIdentity | undefined>;
11
+ }
12
+ export declare class MoveRefusedError extends Error {
13
+ readonly code: "move_repository_mismatch";
14
+ constructor(targetCwd: string, sourceCwd: string);
15
+ }
16
+ export declare class MoveTargetOccupiedError extends Error {
17
+ readonly code = "move_target_occupied";
18
+ constructor(target: string);
19
+ }
20
+ /** The sender's question, answered with the session file as the authority: a committed import is accepted. */
21
+ export declare function answerMoveStatus(ledger: MoveLedger, moveId: MoveId, sessionFile: string | undefined): Promise<MoveStatus>;
22
+ /**
23
+ * Brings a moved session in. The bundle is verified, the folder must be the same repository (the `--rebind` rule),
24
+ * and everything is assembled in a staging folder: the session file gains its queued input back and then
25
+ * `session_moved_in`, so the rename into the session directory, run under the move's ledger lock, is the single
26
+ * commit point. A read-only copy of the same session already here (it moved away earlier) is replaced; a writable
27
+ * one is refused. Importing a bundle already imported returns the session without changing it.
28
+ */
29
+ export declare function importMovedSession(options: ImportMovedSessionOptions): Promise<{
30
+ readonly sessionFile: string;
31
+ }>;
32
+ //# sourceMappingURL=import.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { normalizePath, resolvePath } from "../../utils/paths.js";
4
+ import { compareRepositoryIdentities, readRepositoryIdentity, } from "../repository-identity.js";
5
+ import { getDefaultSessionDir, loadEntriesFromFile, SessionManager } from "../session-manager.js";
6
+ import { rebindSessionFile } from "../session-rebind.js";
7
+ import { encodedSessionId } from "../session-sidecar-store.js";
8
+ import { MoveBundleInvalidError, readManifest, verifyBundle } from "./bundle.js";
9
+ import { MoveFencedError } from "./move-ledger.js";
10
+ import { headerSessionId, SESSION_MOVED_IN, sessionOwnership } from "./ownership.js";
11
+ import { requeueInput } from "./requeue.js";
12
+ export class MoveRefusedError extends Error {
13
+ constructor(targetCwd, sourceCwd) {
14
+ super(`${targetCwd} is not the same repository the session was recorded in (${sourceCwd}). Pick a checkout of the same repository, or copy the session into this folder instead.`);
15
+ this.name = "MoveRefusedError";
16
+ this.code = "move_repository_mismatch";
17
+ }
18
+ }
19
+ export class MoveTargetOccupiedError extends Error {
20
+ constructor(target) {
21
+ super(`A session that can still be written already sits at ${target}; the moved session was not imported over it. Close or move that session first, then send it again.`);
22
+ this.code = "move_target_occupied";
23
+ this.name = "MoveTargetOccupiedError";
24
+ }
25
+ }
26
+ const movedInFor = (file, moveId) => loadEntriesFromFile(file).some((entry) => entry.type === "custom" &&
27
+ entry.customType === SESSION_MOVED_IN &&
28
+ entry.data?.move_id === moveId);
29
+ /** The sender's question, answered with the session file as the authority: a committed import is accepted. */
30
+ export function answerMoveStatus(ledger, moveId, sessionFile) {
31
+ const movedIn = sessionFile !== undefined && existsSync(sessionFile) && movedInFor(sessionFile, moveId);
32
+ return ledger.answerStatus(moveId, { movedIn });
33
+ }
34
+ /**
35
+ * Brings a moved session in. The bundle is verified, the folder must be the same repository (the `--rebind` rule),
36
+ * and everything is assembled in a staging folder: the session file gains its queued input back and then
37
+ * `session_moved_in`, so the rename into the session directory, run under the move's ledger lock, is the single
38
+ * commit point. A read-only copy of the same session already here (it moved away earlier) is replaced; a writable
39
+ * one is refused. Importing a bundle already imported returns the session without changing it.
40
+ */
41
+ export async function importMovedSession(options) {
42
+ const manifest = readManifest(options.bundleDir);
43
+ const targetCwd = resolvePath(options.targetCwd);
44
+ const sessionDir = options.sessionDir ? normalizePath(options.sessionDir) : getDefaultSessionDir(targetCwd);
45
+ const target = join(sessionDir, manifest.sessionFileName);
46
+ if (existsSync(target) && movedInFor(target, manifest.moveId)) {
47
+ // The rename already committed; a crash may have lost the ledger write, and the file is the authority.
48
+ await options.ledger.commit(manifest.moveId, () => undefined);
49
+ return { sessionFile: target };
50
+ }
51
+ try {
52
+ verifyBundle(options.bundleDir, manifest);
53
+ }
54
+ catch (error) {
55
+ if (error instanceof MoveBundleInvalidError)
56
+ await options.ledger.fail(manifest.moveId, "bundle_invalid");
57
+ throw error;
58
+ }
59
+ const read = options.readIdentity ?? readRepositoryIdentity;
60
+ if (compareRepositoryIdentities(manifest.repository, await read(targetCwd)) !== "same") {
61
+ await options.ledger.decline(manifest.moveId, "repository");
62
+ throw new MoveRefusedError(targetCwd, manifest.sourceCwd);
63
+ }
64
+ await options.ledger.beginReceive(manifest.moveId);
65
+ const staging = join(sessionDir, ".move-staging", encodeURIComponent(manifest.moveId));
66
+ try {
67
+ const staged = await stage(options.bundleDir, manifest, staging, options.from ?? "");
68
+ const sessionFile = await options.ledger.commit(manifest.moveId, () => {
69
+ replacePredecessor(target, manifest.sessionId, sessionDir);
70
+ return rebindSessionFile(staged, targetCwd, sessionDir);
71
+ });
72
+ rmSync(staging, { recursive: true, force: true });
73
+ return { sessionFile };
74
+ }
75
+ catch (error) {
76
+ if (!(error instanceof MoveFencedError))
77
+ await options.ledger.fail(manifest.moveId, "import_failed");
78
+ throw error;
79
+ }
80
+ }
81
+ /**
82
+ * Only a read-only copy of the same session may be replaced; its sidecars go too so the moved ones take their place.
83
+ * It is removed before the rename: if the rename then fails, the move is recorded `import_failed`, the sender takes
84
+ * the session back, and nothing writable was lost here (the removed copy could not be written).
85
+ */
86
+ function replacePredecessor(target, sessionId, sessionDir) {
87
+ if (!existsSync(target))
88
+ return;
89
+ const entries = loadEntriesFromFile(target);
90
+ if (headerSessionId(entries) !== sessionId || sessionOwnership(entries).writable) {
91
+ throw new MoveTargetOccupiedError(target);
92
+ }
93
+ const extensions = join(sessionDir, "extensions");
94
+ if (existsSync(extensions)) {
95
+ for (const extension of readdirSync(extensions, { withFileTypes: true })) {
96
+ if (extension.isDirectory())
97
+ rmSync(join(extensions, extension.name, `${encodedSessionId(sessionId)}.json`), { force: true });
98
+ }
99
+ }
100
+ rmSync(join(sessionDir, "resident-blobs", sessionId), { recursive: true, force: true });
101
+ rmSync(target, { force: true });
102
+ }
103
+ async function stage(bundleDir, manifest, staging, from) {
104
+ const file = join(staging, manifest.sessionFileName);
105
+ if (!(existsSync(file) && movedInFor(file, manifest.moveId))) {
106
+ rmSync(staging, { recursive: true, force: true });
107
+ for (const item of [manifest.session, ...manifest.sidecars]) {
108
+ const to = item === manifest.session ? file : join(staging, item.path.replace(/^files\//, ""));
109
+ mkdirSync(dirname(to), { recursive: true });
110
+ copyFileSync(join(bundleDir, item.path), to);
111
+ }
112
+ SessionManager.open(file, staging).appendCustomEntry(SESSION_MOVED_IN, {
113
+ move_id: manifest.moveId,
114
+ session_id: manifest.sessionId,
115
+ from,
116
+ bundle_sha256: manifest.session.sha256,
117
+ moved_in_at: new Date().toISOString(),
118
+ });
119
+ }
120
+ // Ids are stable per move, so a staging pass that crashed after some of these replays them instead of doubling.
121
+ await requeueInput(file, manifest.moveId, manifest.droppedUserMessages);
122
+ return file;
123
+ }
124
+ //# sourceMappingURL=import.js.map
@@ -0,0 +1,47 @@
1
+ import type { MoveId } from "./ownership.ts";
2
+ /**
3
+ * The receiving device's record of every move it was offered. Asking about a move is itself a fence: a move this
4
+ * device never heard of is recorded as declined BEFORE the answer leaves, so a later import of it is refused and the
5
+ * sender may safely take its session back. Every writer and the import's final rename share one lock per move.
6
+ */
7
+ export type DeclineReason = "no_record" | "expired" | "cancelled" | "chunk_without_ready" | "repository";
8
+ export type FailReason = "bundle_invalid" | "import_failed";
9
+ export type MoveRecord = {
10
+ readonly state: "receiving";
11
+ } | {
12
+ readonly state: "accepted";
13
+ } | {
14
+ readonly state: "declined";
15
+ readonly reason: DeclineReason;
16
+ } | {
17
+ readonly state: "failed";
18
+ readonly reason: FailReason;
19
+ };
20
+ export type MoveStatus = MoveRecord;
21
+ export declare class MoveFencedError extends Error {
22
+ readonly code = "move_fenced";
23
+ readonly record: MoveRecord;
24
+ constructor(moveId: MoveId, record: MoveRecord);
25
+ }
26
+ export declare class MoveLedger {
27
+ readonly dir: string;
28
+ constructor(dir: string);
29
+ /** Starts receiving a move; refused once the move was answered with anything but `receiving`. */
30
+ beginReceive(moveId: MoveId): Promise<void>;
31
+ /** The signed answer a sender gets; `movedIn` is whether the session file here already records the move. */
32
+ answerStatus(moveId: MoveId, here: {
33
+ readonly movedIn: boolean;
34
+ }): Promise<MoveStatus>;
35
+ /** Cancel, expiry or a refused chunk: only before the bundle was verified and receiving began, never after. */
36
+ decline(moveId: MoveId, reason: DeclineReason): Promise<MoveRecord>;
37
+ /** A verified import that could not finish; it may end a receiving move but never an accepted one. */
38
+ fail(moveId: MoveId, reason: FailReason): Promise<MoveRecord>;
39
+ /** Runs the import's commit (the atomic rename) under the move's lock, then records it accepted. */
40
+ commit<T>(moveId: MoveId, rename: () => T | Promise<T>): Promise<T>;
41
+ private settle;
42
+ private path;
43
+ private read;
44
+ private write;
45
+ private locked;
46
+ }
47
+ //# sourceMappingURL=move-ledger.d.ts.map