@indigoai-us/hq-cloud 6.14.20 → 6.14.22

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/src/s3.ts CHANGED
@@ -1003,6 +1003,59 @@ export interface DownloadModeWarning {
1003
1003
  fallback?: "preserved-local-mode" | "receiver-default";
1004
1004
  }
1005
1005
 
1006
+ /**
1007
+ * A download whose parent directory exists as a DANGLING symlink.
1008
+ *
1009
+ * The vault stores directory symlinks as first-class objects (e.g.
1010
+ * `companies/<co>/.obsidian -> ../../.obsidian`) AND stores files beneath them.
1011
+ * When the link's target is absent on this machine, the child write fails —
1012
+ * and it fails as ENOENT rather than EEXIST, because `mkdir(2)` returns EEXIST
1013
+ * for the link and Node's `recursive: true` implementation then stats it, which
1014
+ * fails on a dangling link. Raw, that surfaced as a hard error that marked the
1015
+ * WHOLE company `errored`/`partial` on every sync cycle (2026-07-24 dogfood box:
1016
+ * `.obsidian/hotkeys.json: ENOENT ... mkdir '.../companies/indigo/.obsidian'`).
1017
+ *
1018
+ * It is a per-object condition, not a company-level failure, so it is typed and
1019
+ * the pull loop skips the object loudly instead of failing the run. We do NOT
1020
+ * materialize the link target: the target is by definition outside the
1021
+ * directory being synced, and creating it would write through a path the
1022
+ * caller's containment guard deliberately refuses.
1023
+ */
1024
+ export class DanglingSymlinkParentError extends Error {
1025
+ readonly key: string;
1026
+ readonly dir: string;
1027
+ constructor(key: string, dir: string) {
1028
+ super(
1029
+ `download skipped: parent directory is a dangling symlink (${dir}) — ` +
1030
+ `the vault stores it as a link whose target is absent locally`,
1031
+ );
1032
+ this.name = "DanglingSymlinkParentError";
1033
+ this.key = key;
1034
+ this.dir = dir;
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * Create the parent directory for a download, distinguishing the
1040
+ * dangling-symlink case from a genuine mkdir failure.
1041
+ *
1042
+ * Callers must have already established that `dir` does not resolve
1043
+ * (`fs.existsSync(dir) === false`); `existsSync` FOLLOWS symlinks, so a
1044
+ * dangling link reports absent here while `lstat` still sees the link.
1045
+ */
1046
+ function ensureDownloadParentDir(dir: string, key: string): void {
1047
+ let link: fs.Stats | null = null;
1048
+ try {
1049
+ link = fs.lstatSync(dir);
1050
+ } catch {
1051
+ // Genuinely absent — the ordinary first-write path.
1052
+ }
1053
+ if (link?.isSymbolicLink()) {
1054
+ throw new DanglingSymlinkParentError(key, dir);
1055
+ }
1056
+ fs.mkdirSync(dir, { recursive: true });
1057
+ }
1058
+
1006
1059
  export async function downloadFile(
1007
1060
  ctx: EntityContext,
1008
1061
  key: string,
@@ -1032,7 +1085,7 @@ export async function downloadFile(
1032
1085
 
1033
1086
  const dir = path.dirname(localPath);
1034
1087
  if (!fs.existsSync(dir)) {
1035
- fs.mkdirSync(dir, { recursive: true });
1088
+ ensureDownloadParentDir(dir, key);
1036
1089
  }
1037
1090
 
1038
1091
  // Symlink path: presence of SYMLINK_TARGET_META_KEY (any non-empty