@tangle-network/agent-profile-materialize 0.14.1 → 0.15.0

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/dist/index.d.ts CHANGED
@@ -309,6 +309,16 @@ export interface ApplyWorkspacePlanOptions {
309
309
  runtimeOnlyFiles?: readonly string[];
310
310
  /** Existing workspace paths are either exact idempotent inputs or forbidden. */
311
311
  existingFiles?: "allow-identical" | "reject";
312
+ /**
313
+ * `workspacePlanDigest` of a plan a prior call already applied into this
314
+ * workspace. When it equals this plan's digest, application writes no files:
315
+ * the workspace holds the running agent's live state, and a file the agent
316
+ * edited or removed since the prior application stays exactly as the agent
317
+ * left it. Env, flags, prompt intents, and the receipt are still produced.
318
+ * A different digest applies normally, so a plan that changed mid-session
319
+ * still meets the existing-file refusal.
320
+ */
321
+ appliedPlanDigest?: string;
312
322
  }
313
323
  /** Fail when a profile asks for controls the chosen harness cannot execute. */
314
324
  export declare function assertWorkspacePlanSupported(plan: Pick<WorkspacePlan, "unsupported">): void;
package/dist/index.js CHANGED
@@ -1894,6 +1894,28 @@ function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, opti
1894
1894
  ...file,
1895
1895
  absPath: resolveWorkspaceFilePath(workspaceRoot, file.relPath),
1896
1896
  }));
1897
+ // A prior application of THIS EXACT plan makes the workspace the running
1898
+ // agent's live state: a file the agent edited or removed since then is the
1899
+ // session's own memory, not a conflict, so a byte comparison against the
1900
+ // plan would refuse a resume the caller proved is unchanged. The digest the
1901
+ // caller passes is its own record of what it already applied here; only the
1902
+ // caller can know that, which is why this is an option and not an inference
1903
+ // from disk. `reject` is not short-circuited: that policy asserts an empty
1904
+ // target and must keep failing on any pre-existing path.
1905
+ if (existingFiles === "allow-identical" &&
1906
+ options.appliedPlanDigest !== undefined &&
1907
+ options.appliedPlanDigest === publicPlanDigest) {
1908
+ return {
1909
+ receipt: safeWorkspacePlanReceipt(publicPlan, files.map((file) => file.relPath), publicPlanDigest),
1910
+ spawn: {
1911
+ env: { ...resolvedPlan.env },
1912
+ flags: [...resolvedPlan.flags],
1913
+ ...promptIntentFields(resolvedPlan, samePromptIntentValue),
1914
+ ...systemPromptBindingField(resolvedPlan),
1915
+ },
1916
+ runtimeOnlyFiles: privateRuntimeFiles,
1917
+ };
1918
+ }
1897
1919
  // A path this package invented can only hold this package's own earlier
1898
1920
  // output, so re-applying into the same workspace replaces it rather than
1899
1921
  // failing the turn. `reject` still refuses: that policy is "nothing may
@@ -1906,7 +1928,7 @@ function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, opti
1906
1928
  if (workspaceFileExists(file.absPath) &&
1907
1929
  !mayReplace(file) &&
1908
1930
  (existingFiles === "reject" || !workspaceFileMatches(file))) {
1909
- throw new Error(`Refusing to replace existing workspace file: ${file.relPath}`);
1931
+ throw new Error(workspaceFileConflictMessage(file, existingFiles, publicPlanDigest));
1910
1932
  }
1911
1933
  }
1912
1934
  // Parent directories remain on failure because they may contain concurrent
@@ -1956,7 +1978,7 @@ function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, opti
1956
1978
  continue;
1957
1979
  }
1958
1980
  if (isFsError(error, "EEXIST")) {
1959
- throw new Error(`Refusing to replace existing workspace file: ${entry.file.relPath}`, { cause: error });
1981
+ throw new Error(workspaceFileConflictMessage(entry.file, existingFiles, publicPlanDigest), { cause: error });
1960
1982
  }
1961
1983
  throw error;
1962
1984
  }
@@ -2033,6 +2055,42 @@ function workspaceFileMatches(file) {
2033
2055
  (stats.mode & 0o777) === file.mode &&
2034
2056
  readFileSync(file.absPath, "utf8") === file.content);
2035
2057
  }
2058
+ function sha256Hex(content) {
2059
+ return createHash("sha256").update(content).digest("hex");
2060
+ }
2061
+ /**
2062
+ * The refusal names what differs — planned versus existing content digest,
2063
+ * mode, or file kind — plus the plan digest, so the operator can tell a
2064
+ * mid-session profile swap from an agent-edited workspace file without
2065
+ * reproducing the workspace. Reads here are best-effort diagnosis inside an
2066
+ * already-failing path; an unreadable existing file is reported, not thrown
2067
+ * over.
2068
+ */
2069
+ function workspaceFileConflictMessage(file, existingFiles, planDigest) {
2070
+ const prefix = `Refusing to replace existing workspace file: ${file.relPath}`;
2071
+ if (existingFiles === "reject") {
2072
+ return `${prefix} — the reject policy forbids any pre-existing plan path (plan ${planDigest})`;
2073
+ }
2074
+ const planned = `planned sha256:${sha256Hex(file.content)} mode ${file.mode.toString(8)}`;
2075
+ let existing;
2076
+ try {
2077
+ const stats = lstatSync(file.absPath);
2078
+ if (!stats.isFile()) {
2079
+ existing = stats.isSymbolicLink()
2080
+ ? "existing path is a symlink, not a regular file"
2081
+ : "existing path is not a regular file";
2082
+ }
2083
+ else {
2084
+ const mode = (stats.mode & 0o777).toString(8);
2085
+ existing = `existing sha256:${sha256Hex(readFileSync(file.absPath))} mode ${mode}`;
2086
+ }
2087
+ }
2088
+ catch (error) {
2089
+ const message = error instanceof Error ? error.message : String(error);
2090
+ existing = `existing file unreadable: ${message}`;
2091
+ }
2092
+ return `${prefix} — ${planned} != ${existing} (plan ${planDigest})`;
2093
+ }
2036
2094
  function prepareWorkspaceFile(workspaceRoot, file) {
2037
2095
  assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
2038
2096
  const temporaryPath = join(dirname(file.absPath), `.agent-profile-${randomUUID()}.tmp`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,13 +20,13 @@
20
20
  "dist"
21
21
  ],
22
22
  "devDependencies": {
23
- "@tangle-network/agent-interface": "0.51.0",
23
+ "@tangle-network/agent-interface": "0.52.0",
24
24
  "@types/node": "25.6.0",
25
25
  "typescript": "^6.0.3",
26
26
  "vitest": "^4.1.10"
27
27
  },
28
28
  "peerDependencies": {
29
- "@tangle-network/agent-interface": ">=0.47.0 <0.52.0"
29
+ "@tangle-network/agent-interface": ">=0.47.0 <0.53.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json",