MCPcopy
hub / github.com/coder/mux / saveFile

Method saveFile

src/node/services/memoryService.ts:947–1002  ·  view source on GitHub ↗

* 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
  )

Source from the content-addressed store, hash-verified

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 // -------------------------------------------------------------------------

Callers 4

writeInboxFunction · 0.80
routerFunction · 0.80

Calls 13

requireFilePathMethod · 0.95
resolveStoreMethod · 0.95
readBoundedTextFileMethod · 0.95
recordUsageMethod · 0.95
emitChangeMethod · 0.95
getErrorMessageFunction · 0.90
parseMemoryPathFunction · 0.85
assertWithinFileSizeCapFunction · 0.85
sha256HexFunction · 0.85
withLockMethod · 0.80
kindMethod · 0.65
listFilesMethod · 0.65

Tested by

no test coverage detected