* Whole-file save from the Memory tab. expectedSha256 is the sha captured at * load time (null = "I am creating a new file"); mismatches are conflicts so * concurrent agent edits never get silently overwritten.
(
ctx: MemoryScopeContext,
virtualPath: string,
content: string,
expectedSha256: string | null,
actor: MemoryActor
)
| 945 | * concurrent agent edits never get silently overwritten. |
| 946 | */ |
| 947 | async saveFile( |
| 948 | ctx: MemoryScopeContext, |
| 949 | virtualPath: string, |
| 950 | content: string, |
| 951 | expectedSha256: string | null, |
| 952 | actor: MemoryActor |
| 953 | ): Promise<MemorySaveFileResult> { |
| 954 | const conflict = (message: string): MemorySaveFileResult => ({ |
| 955 | success: false, |
| 956 | error: { kind: "conflict", message }, |
| 957 | }); |
| 958 | try { |
| 959 | const parsed = parseMemoryPath(virtualPath); |
| 960 | const scope = this.requireFilePath(parsed, virtualPath); |
| 961 | assertWithinFileSizeCap(content); |
| 962 | // UI save can create new files: materialize the scope root on first use. |
| 963 | const store = await this.resolveStore(ctx, scope, parsed.relPath, { createRoot: true }); |
| 964 | return await this.locks.withLock(store.physicalRoot, async () => { |
| 965 | const kind = await store.kind(parsed.relPath); |
| 966 | if (kind === "dir") { |
| 967 | throw new MemoryCommandError(`${virtualPath} is a directory, not a file`); |
| 968 | } |
| 969 | if (expectedSha256 === null) { |
| 970 | if (kind !== null) { |
| 971 | return conflict(`A file already exists at ${virtualPath}; reload before saving`); |
| 972 | } |
| 973 | const files = await store.listFiles(); |
| 974 | if (files.length >= MEMORY_MAX_FILES_PER_SCOPE) { |
| 975 | throw new MemoryCommandError( |
| 976 | `The ${scope} memory scope is full (${MEMORY_MAX_FILES_PER_SCOPE} files); delete unused files first` |
| 977 | ); |
| 978 | } |
| 979 | } else { |
| 980 | if (kind === null) { |
| 981 | return conflict(`${virtualPath} no longer exists; it may have been deleted`); |
| 982 | } |
| 983 | const current = await this.readBoundedTextFile(store, parsed.relPath, virtualPath); |
| 984 | if (sha256Hex(current) !== expectedSha256) { |
| 985 | return conflict( |
| 986 | `${virtualPath} changed since it was loaded; reload and re-apply your edits` |
| 987 | ); |
| 988 | } |
| 989 | } |
| 990 | await store.writeFile(parsed.relPath, content); |
| 991 | await this.recordUsage(ctx, scope, parsed.relPath, { write: true }); |
| 992 | this.emitChange(ctx, scope, parsed.relPath, actor); |
| 993 | return { success: true as const, data: { sha256: sha256Hex(content) } }; |
| 994 | }); |
| 995 | } catch (error) { |
| 996 | const message = |
| 997 | error instanceof MemoryCommandError |
| 998 | ? error.message |
| 999 | : `Memory operation failed: ${getErrorMessage(error)}`; |
| 1000 | return { success: false, error: { kind: "error", message } }; |
| 1001 | } |
| 1002 | } |
| 1003 | |
| 1004 | // ------------------------------------------------------------------------- |
no test coverage detected