@animalabs/agent-framework 0.29.0 → 0.31.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.
Files changed (236) hide show
  1. package/README.md +71 -8
  2. package/dist/src/admission/admit.d.ts +90 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -0
  4. package/dist/src/admission/admit.js +43 -0
  5. package/dist/src/admission/admit.js.map +1 -0
  6. package/dist/src/agent.d.ts +50 -9
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +125 -23
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +10 -3
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/author-presentation.d.ts +50 -0
  14. package/dist/src/author-presentation.d.ts.map +1 -0
  15. package/dist/src/author-presentation.js +58 -0
  16. package/dist/src/author-presentation.js.map +1 -0
  17. package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/render.js +7 -6
  19. package/dist/src/code-execution/isolate/render.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +5 -4
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/dendrite/index.d.ts +6 -0
  24. package/dist/src/dendrite/index.d.ts.map +1 -0
  25. package/dist/src/dendrite/index.js +3 -0
  26. package/dist/src/dendrite/index.js.map +1 -0
  27. package/dist/src/dendrite/presets.d.ts +60 -0
  28. package/dist/src/dendrite/presets.d.ts.map +1 -0
  29. package/dist/src/dendrite/presets.js +117 -0
  30. package/dist/src/dendrite/presets.js.map +1 -0
  31. package/dist/src/dendrite/registry.d.ts +260 -0
  32. package/dist/src/dendrite/registry.d.ts.map +1 -0
  33. package/dist/src/dendrite/registry.js +742 -0
  34. package/dist/src/dendrite/registry.js.map +1 -0
  35. package/dist/src/dendrite/types.d.ts +392 -0
  36. package/dist/src/dendrite/types.d.ts.map +1 -0
  37. package/dist/src/dendrite/types.js +24 -0
  38. package/dist/src/dendrite/types.js.map +1 -0
  39. package/dist/src/framework.d.ts +806 -118
  40. package/dist/src/framework.d.ts.map +1 -1
  41. package/dist/src/framework.js +4180 -1121
  42. package/dist/src/framework.js.map +1 -1
  43. package/dist/src/gate/event-gate.d.ts +36 -21
  44. package/dist/src/gate/event-gate.d.ts.map +1 -1
  45. package/dist/src/gate/event-gate.js +223 -149
  46. package/dist/src/gate/event-gate.js.map +1 -1
  47. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
  48. package/dist/src/gate/recipe-reconcile.js +6 -5
  49. package/dist/src/gate/recipe-reconcile.js.map +1 -1
  50. package/dist/src/host-presentation.d.ts +4 -0
  51. package/dist/src/host-presentation.d.ts.map +1 -1
  52. package/dist/src/host-presentation.js +12 -10
  53. package/dist/src/host-presentation.js.map +1 -1
  54. package/dist/src/index.d.ts +6 -2
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/index.js +7 -1
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/inference-bounds.d.ts +37 -0
  59. package/dist/src/inference-bounds.d.ts.map +1 -0
  60. package/dist/src/inference-bounds.js +84 -0
  61. package/dist/src/inference-bounds.js.map +1 -0
  62. package/dist/src/line-reader.d.ts +7 -1
  63. package/dist/src/line-reader.d.ts.map +1 -1
  64. package/dist/src/line-reader.js +17 -6
  65. package/dist/src/line-reader.js.map +1 -1
  66. package/dist/src/mcpl/channel-registry.d.ts +84 -9
  67. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  68. package/dist/src/mcpl/channel-registry.js +136 -58
  69. package/dist/src/mcpl/channel-registry.js.map +1 -1
  70. package/dist/src/mcpl/errors.d.ts +1 -0
  71. package/dist/src/mcpl/errors.d.ts.map +1 -1
  72. package/dist/src/mcpl/errors.js +3 -0
  73. package/dist/src/mcpl/errors.js.map +1 -1
  74. package/dist/src/mcpl/inbound-source.d.ts +2 -2
  75. package/dist/src/mcpl/inbound-source.js +2 -2
  76. package/dist/src/mcpl/inference-router.d.ts +11 -1
  77. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  78. package/dist/src/mcpl/inference-router.js +65 -9
  79. package/dist/src/mcpl/inference-router.js.map +1 -1
  80. package/dist/src/mcpl/modern-connection.d.ts +10 -7
  81. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  82. package/dist/src/mcpl/modern-connection.js +85 -74
  83. package/dist/src/mcpl/modern-connection.js.map +1 -1
  84. package/dist/src/mcpl/protocol-family.d.ts +1 -1
  85. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  86. package/dist/src/mcpl/protocol-family.js +45 -3
  87. package/dist/src/mcpl/protocol-family.js.map +1 -1
  88. package/dist/src/mcpl/push-coalescer.d.ts +11 -7
  89. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  90. package/dist/src/mcpl/push-coalescer.js +21 -0
  91. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  92. package/dist/src/mcpl/references.d.ts +1 -2
  93. package/dist/src/mcpl/references.d.ts.map +1 -1
  94. package/dist/src/mcpl/references.js +3 -3
  95. package/dist/src/mcpl/references.js.map +1 -1
  96. package/dist/src/mcpl/server-connection.d.ts +14 -0
  97. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  98. package/dist/src/mcpl/server-connection.js +26 -22
  99. package/dist/src/mcpl/server-connection.js.map +1 -1
  100. package/dist/src/mcpl/tool-signature.d.ts +1 -0
  101. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  102. package/dist/src/mcpl/tool-signature.js +16 -11
  103. package/dist/src/mcpl/tool-signature.js.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  105. package/dist/src/mcpl/toolsets-module.js +7 -0
  106. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  107. package/dist/src/mcpl/transport.d.ts +44 -21
  108. package/dist/src/mcpl/transport.d.ts.map +1 -1
  109. package/dist/src/mcpl/transport.js +283 -64
  110. package/dist/src/mcpl/transport.js.map +1 -1
  111. package/dist/src/mcpl/types.d.ts +17 -1
  112. package/dist/src/mcpl/types.d.ts.map +1 -1
  113. package/dist/src/mcpl/types.js.map +1 -1
  114. package/dist/src/module-registry.d.ts +32 -4
  115. package/dist/src/module-registry.d.ts.map +1 -1
  116. package/dist/src/module-registry.js +189 -25
  117. package/dist/src/module-registry.js.map +1 -1
  118. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  119. package/dist/src/modules/discord/discord-js-client.js +24 -16
  120. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  121. package/dist/src/modules/sender-name.d.ts +5 -4
  122. package/dist/src/modules/sender-name.d.ts.map +1 -1
  123. package/dist/src/modules/sender-name.js +7 -5
  124. package/dist/src/modules/sender-name.js.map +1 -1
  125. package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
  126. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  127. package/dist/src/modules/workspace/disk-agreement.js +33 -12
  128. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  129. package/dist/src/modules/workspace/index.d.ts +6 -3
  130. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/index.js +61 -15
  132. package/dist/src/modules/workspace/index.js.map +1 -1
  133. package/dist/src/modules/workspace/observe.d.ts +9 -1
  134. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  135. package/dist/src/modules/workspace/observe.js +50 -10
  136. package/dist/src/modules/workspace/observe.js.map +1 -1
  137. package/dist/src/modules/workspace/reconcile.d.ts +11 -1
  138. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  139. package/dist/src/modules/workspace/reconcile.js +19 -8
  140. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  141. package/dist/src/modules/workspace/types.d.ts +8 -0
  142. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  143. package/dist/src/modules/workspace/types.js.map +1 -1
  144. package/dist/src/modules/workspace/watcher.d.ts +7 -0
  145. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/watcher.js +7 -5
  147. package/dist/src/modules/workspace/watcher.js.map +1 -1
  148. package/dist/src/notice-tags.d.ts +2 -0
  149. package/dist/src/notice-tags.d.ts.map +1 -1
  150. package/dist/src/notice-tags.js +2 -0
  151. package/dist/src/notice-tags.js.map +1 -1
  152. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  153. package/dist/src/recovery/discord-awareness-outbox.js +1 -9
  154. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  155. package/dist/src/silent-heartbeat.d.ts +4 -2
  156. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  157. package/dist/src/silent-heartbeat.js +7 -4
  158. package/dist/src/silent-heartbeat.js.map +1 -1
  159. package/dist/src/stored-injections.d.ts +4 -0
  160. package/dist/src/stored-injections.d.ts.map +1 -1
  161. package/dist/src/stored-injections.js +6 -0
  162. package/dist/src/stored-injections.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +2 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -1
  165. package/dist/src/supplied-text.js +16 -7
  166. package/dist/src/supplied-text.js.map +1 -1
  167. package/dist/src/tool-result-guard.js +1 -1
  168. package/dist/src/tool-result-guard.js.map +1 -1
  169. package/dist/src/tune-out/coordinator.d.ts +368 -16
  170. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  171. package/dist/src/tune-out/coordinator.js +1224 -136
  172. package/dist/src/tune-out/coordinator.js.map +1 -1
  173. package/dist/src/tune-out/tools.d.ts +115 -7
  174. package/dist/src/tune-out/tools.d.ts.map +1 -1
  175. package/dist/src/tune-out/tools.js +55 -18
  176. package/dist/src/tune-out/tools.js.map +1 -1
  177. package/dist/src/types/agent.d.ts +27 -1
  178. package/dist/src/types/agent.d.ts.map +1 -1
  179. package/dist/src/types/framework.d.ts +153 -15
  180. package/dist/src/types/framework.d.ts.map +1 -1
  181. package/dist/src/types/module.d.ts +42 -3
  182. package/dist/src/types/module.d.ts.map +1 -1
  183. package/dist/src/types/trace.d.ts +140 -3
  184. package/dist/src/types/trace.d.ts.map +1 -1
  185. package/package.json +4 -4
  186. package/src/admission/admit.ts +113 -0
  187. package/src/agent.ts +138 -31
  188. package/src/api/mcp-server-core.ts +10 -3
  189. package/src/author-presentation.ts +81 -0
  190. package/src/code-execution/isolate/render.ts +7 -6
  191. package/src/code-execution/isolate/scripts.ts +5 -4
  192. package/src/dendrite/index.ts +32 -0
  193. package/src/dendrite/presets.ts +149 -0
  194. package/src/dendrite/registry.ts +808 -0
  195. package/src/dendrite/types.ts +397 -0
  196. package/src/framework.ts +4344 -1052
  197. package/src/gate/event-gate.ts +251 -150
  198. package/src/gate/recipe-reconcile.ts +7 -5
  199. package/src/host-presentation.ts +14 -10
  200. package/src/index.ts +16 -2
  201. package/src/inference-bounds.ts +111 -0
  202. package/src/line-reader.ts +22 -5
  203. package/src/mcpl/channel-registry.ts +170 -60
  204. package/src/mcpl/errors.ts +4 -0
  205. package/src/mcpl/inbound-source.ts +2 -2
  206. package/src/mcpl/inference-router.ts +67 -10
  207. package/src/mcpl/modern-connection.ts +83 -68
  208. package/src/mcpl/protocol-family.ts +37 -4
  209. package/src/mcpl/push-coalescer.ts +21 -7
  210. package/src/mcpl/references.ts +3 -4
  211. package/src/mcpl/server-connection.ts +30 -21
  212. package/src/mcpl/tool-signature.ts +18 -11
  213. package/src/mcpl/toolsets-module.ts +7 -0
  214. package/src/mcpl/transport.ts +261 -60
  215. package/src/mcpl/types.ts +18 -1
  216. package/src/module-registry.ts +178 -25
  217. package/src/modules/discord/discord-js-client.ts +33 -29
  218. package/src/modules/sender-name.ts +8 -5
  219. package/src/modules/workspace/disk-agreement.ts +39 -15
  220. package/src/modules/workspace/index.ts +65 -14
  221. package/src/modules/workspace/observe.ts +51 -10
  222. package/src/modules/workspace/reconcile.ts +22 -8
  223. package/src/modules/workspace/types.ts +5 -0
  224. package/src/modules/workspace/watcher.ts +8 -6
  225. package/src/notice-tags.ts +2 -0
  226. package/src/recovery/discord-awareness-outbox.ts +1 -11
  227. package/src/silent-heartbeat.ts +7 -4
  228. package/src/stored-injections.ts +7 -0
  229. package/src/supplied-text.ts +17 -6
  230. package/src/tool-result-guard.ts +1 -1
  231. package/src/tune-out/coordinator.ts +1374 -141
  232. package/src/tune-out/tools.ts +166 -13
  233. package/src/types/agent.ts +30 -1
  234. package/src/types/framework.ts +157 -15
  235. package/src/types/module.ts +42 -3
  236. package/src/types/trace.ts +154 -3
@@ -5,15 +5,16 @@
5
5
  * auto-sync between real filesystem and Chronicle, and manual materialization.
6
6
  */
7
7
  import { NOTICE_TAG } from '../../notice-tags.js';
8
+ import { nameToTypeBack } from '../../supplied-text.js';
8
9
  import { constants as fsConstants } from 'node:fs';
9
10
  import { open, readdir, readlink, stat, lstat, realpath } from 'node:fs/promises';
10
11
  import { basename, dirname, isAbsolute, join, parse, resolve, relative, sep } from 'node:path';
11
12
  import { WORKSPACE_FS_EVENT_TYPES, opToEventType } from './types.js';
12
13
  import { MountWatcher } from './watcher.js';
13
14
  import { hashContent, storedBytes, DEFAULT_MAX_FILE_SIZE } from './sync.js';
14
- import { DiskAgreement, EvidenceUnavailable, callGate, sameRootIdentity } from './disk-agreement.js';
15
+ import { DiskAgreement, EvidenceUnavailable, callGate, rootUnreadable, sameRootIdentity } from './disk-agreement.js';
15
16
  import { BranchIntents } from './branch-intent.js';
16
- import { CHANGED, DIRECTORY, IGNORED, NONBLOCK, OTHER_MOUNT, OUTSIDE_INCLUDE, ROOT_REPLACED, ROOT_UNAVAILABLE, beneathRoot, checkDir, checkRoot, confirmAbsent, coversLanding, directoryAt, diskSpelling, filesystemTrustsCtime, foldsCase, inOtherMount, insideMount, leadsInto, linkOnTheWay, linkRefusal, linkWords, locateRoot, looksBinary, namesHolder, nestedAtReal, nestedGuess, nestedHolder, nestedStep, observePath, parsedSpelling, passedOverAt, pathSegments, patternRefusal, readBytes, readPrefix, restBeneath, rootAbsence, sameRoot, shownLeads, sniffImageMime, surelyAtRoot, walkScope, withinIncludes } from './observe.js';
17
+ import { CHANGED, DIRECTORY, IGNORED, NONBLOCK, OTHER_MOUNT, OUTSIDE_INCLUDE, ROOT_ON_ANOTHER_DEVICE, ROOT_REPLACED, ROOT_UNAVAILABLE, beneathRoot, checkDir, checkRoot, confirmAbsent, coversLanding, directoryAt, diskSpelling, filesystemTrustsCtime, foldsCase, inOtherMount, insideMount, leadsInto, linkOnTheWay, linkRefusal, linkWords, locateRoot, looksBinary, namesHolder, nestedAtReal, nestedGuess, nestedHolder, nestedStep, observePath, parsedSpelling, passedOverAt, pathSegments, patternRefusal, readBytes, readPrefix, restBeneath, rootAbsence, sameRoot, shownLeads, sniffImageMime, surelyAtRoot, walkScope, withinIncludes } from './observe.js';
17
18
  import { TOOL_BATCH_HOOK_TIMEOUT_MS, ensureStateRegistered } from '../../module-registry.js';
18
19
  import { DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS } from '../../tool-result-history.js';
19
20
  import { countBudget, decodeForEdit, hintBudget, planFile } from './edit-plan.js';
@@ -21,7 +22,7 @@ import { cutLine, editList, editReceipt, fitted, refusalText } from './edit-text
21
22
  import { fileText } from './text.js';
22
23
  import { DEFAULT_READ_LINE_LIMIT, characterPage, linePage } from './read-page.js';
23
24
  import { grepText, textAnswer } from './result-text.js';
24
- import { NOT_PUSHED_IN_OTHER_MOUNT, WALK_CAP, entryMode, explained, pushPaths, reconcilePass, settleAdoptionBeforeMutation, wayOn, } from './reconcile.js';
25
+ import { NOT_PUSHED_IN_OTHER_MOUNT, WALK_CAP, entryMode, explained, passRoot, pushPaths, reconcilePass, settleAdoptionBeforeMutation, wayOn, } from './reconcile.js';
25
26
  import { EffectFailed, unlinkContained } from './effects.js';
26
27
  import { AWAITS_REOPEN, MOUNT_IT, ROOT_RECHECK, Refusal, UNFOLLOWED_STEP, changesFiles, didNothing, errnoCode, errnoWayOn, errnoWords, hostFix, onlyTheHost, systemWords, unexpectedAnswer } from './answers.js';
27
28
  import { unknownToolMessage } from '../unknown-tool.js';
@@ -61,6 +62,17 @@ const DIRECT_READS_AT_ONCE = 16;
61
62
  const DIRECT_SYNC_SKIPPED = "a direct mount: its tools read disk as it is, so there's nothing to take in";
62
63
  /** Why materialize passes over a writable direct mount, as its result's `skipped` says. */
63
64
  const DIRECT_MATERIALIZE_SKIPPED = "a direct mount: its writes are on disk as they're made, so there's nothing to write";
65
+ const NOTHING_RECORDED_YET = 'nothing is recorded of this root yet, so any directory at its path is taken as it';
66
+ const NOTHING_RECORDED = `${NOTHING_RECORDED_YET}; the first sync that takes in a file, or materialize that writes one, records it`;
67
+ const NOTHING_RECORDED_READ_ONLY = `${NOTHING_RECORDED_YET}; the first sync that takes in a file records it`;
68
+ const BORN_UNRECORDED = 'no birth time is recorded of this root yet, so a directory made again in its place with the same inode can\'t be told from it; the next sync or ls of this mount records it';
69
+ function asShown(identity) {
70
+ if (identity?.born === undefined)
71
+ return identity;
72
+ const ns = BigInt(identity.born);
73
+ const second = new Date(Number(ns / 1000000000n) * 1000).toISOString().replace(/\.000Z$/, '');
74
+ return { ...identity, born: `${second}.${String(ns % 1000000000n).padStart(9, '0')}Z` };
75
+ }
64
76
  class WorkspaceImageReadError extends Refusal {
65
77
  reason;
66
78
  constructor(reason, wayOn) {
@@ -1106,6 +1118,8 @@ export class WorkspaceModule {
1106
1118
  /** One disk→store pass; the caller holds the mount's turn. Emits its events. */
1107
1119
  async passUnlocked(mount, scope, opts = {}) {
1108
1120
  const result = await reconcilePass(this.getStore(), this.agreementOrThrow(), await this.runtime(mount), scope, opts);
1121
+ if (result.rootListed !== undefined)
1122
+ mount.lastPassRoot = { at: new Date().toISOString(), listed: result.rootListed };
1109
1123
  if (scope.kind === 'dir' && scope.dir === '' && scope.recursive)
1110
1124
  mount.initialSyncDone = true;
1111
1125
  // What a pass says is another mount's directory names that mount and
@@ -2005,14 +2019,14 @@ export class WorkspaceModule {
2005
2019
  return { requestInference: true };
2006
2020
  let text;
2007
2021
  if (paths.length === 1) {
2008
- text = `[${NOTICE_TAG.workspaceEvent} · ${op} · ${paths[0]}]`;
2022
+ text = `[${NOTICE_TAG.workspaceEvent} · ${op} · ${nameToTypeBack(paths[0])}]`;
2009
2023
  }
2010
2024
  else {
2011
- const list = paths.map(p => `- ${p}`).join('\n');
2025
+ const list = paths.map(p => `- ${nameToTypeBack(p)}`).join('\n');
2012
2026
  text = `[${NOTICE_TAG.workspaceEvent} · ${op} · ${paths.length} files in ${mountName}/]\n${list}`;
2013
2027
  }
2014
2028
  if (conflicts && conflicts.length > 0) {
2015
- text += `\n[${NOTICE_TAG.conflictsWithDisk}: ${conflicts.join(', ')}; reading each shows how to settle it]`;
2029
+ text += `\n[${NOTICE_TAG.conflictsWithDisk}: ${conflicts.map(nameToTypeBack).join(', ')}; reading each shows how to settle it]`;
2016
2030
  }
2017
2031
  return {
2018
2032
  requestInference: true,
@@ -2078,7 +2092,7 @@ export class WorkspaceModule {
2078
2092
  /**
2079
2093
  * Write binary content (e.g. an image pulled from the agent's context) to a
2080
2094
  * mount, through the same Chronicle-tree + auto-materialize path as the
2081
- * `write` tool. Public API for the framework's synthesized `save_image`
2095
+ * `write` tool. Public API for the framework's synthesized `save_recent_image`
2082
2096
  * tool and other peer callers that hold bytes rather than text.
2083
2097
  *
2084
2098
  * `branch`, when given, ties the write to that branch: the store's current
@@ -2910,8 +2924,9 @@ export class WorkspaceModule {
2910
2924
  * observation checks it (checkRoot), so the read and the evidence agree on
2911
2925
  * which directory the mount is. Once the workspace has recorded the root's
2912
2926
  * identity, a root that resolves to any other directory is refused, in
2913
- * ROOT_REPLACED's words; with none recorded yet, it need only be a
2914
- * directory there. A refusal where no directory is at the root's path
2927
+ * ROOT_REPLACED's words, or ROOT_ON_ANOTHER_DEVICE's where only its device
2928
+ * differs and birth times can't tell; with none recorded yet, it need only
2929
+ * be a directory there. A refusal where no directory is at the root's path
2915
2930
  * carries the errno that gives (rootAbsence: ENOENT when nothing is there
2916
2931
  * and ENOTDIR when something else is, which a caller can read as no file,
2917
2932
  * or the errno the root's stat failed with otherwise, such as EACCES). A
@@ -3751,9 +3766,7 @@ export class WorkspaceModule {
3751
3766
  const absent = err instanceof WorkspaceReadError && err.code !== 'mount_unavailable' && (err.errno === 'ENOENT' || err.errno === 'ENOTDIR');
3752
3767
  if (!absent)
3753
3768
  return { unreadable: containedRefusal(err, path) };
3754
- const root = resolve(mount.config.path);
3755
- const rootReal = await realpath(root).catch(() => root);
3756
- const view = this.viewOf(mount, this.agreementOrThrow().rootIdentityForRead(mount.config.name));
3769
+ const { view, rootReal } = await this.readView(mount);
3757
3770
  const proof = await confirmAbsent(view, rootReal, relativePath);
3758
3771
  if (!('reason' in proof))
3759
3772
  return 'none';
@@ -5502,6 +5515,8 @@ export class WorkspaceModule {
5502
5515
  if (input.mount !== undefined && !this.mounts.has(input.mount)) {
5503
5516
  return { success: false, error: didNothing('status', this.noSuchMountField(input.mount, 'covers every mount')), isError: true };
5504
5517
  }
5518
+ const asked = input.mount === undefined ? [] : [...this.mounts].filter(([name, mount]) => !mount.direct && name === input.mount);
5519
+ const roots = new Map(await Promise.all(asked.map(async ([name, mount]) => [name, await this.rootState(mount)])));
5505
5520
  for (const [name, mount] of this.mounts) {
5506
5521
  if (input.mount !== undefined && name !== input.mount)
5507
5522
  continue;
@@ -5520,6 +5535,8 @@ export class WorkspaceModule {
5520
5535
  const { conflicts: conflictPaths, deletions: deletionPaths, owed } = this.heldChanges(mount);
5521
5536
  const pending = new Set([...conflictPaths, ...deletionPaths, ...owed]);
5522
5537
  const currentBranch = store.currentBranch();
5538
+ const root = input.mount !== undefined ? roots.get(name)
5539
+ : mount.lastPassRoot?.listed ? { unavailable: mount.lastPassRoot.listed, lastPassAt: mount.lastPassRoot.at } : undefined;
5523
5540
  status[name] = {
5524
5541
  path: mount.config.path,
5525
5542
  mode: mount.config.backend === 'direct' ? mount.config.mode ?? 'read-write' : mount.config.mode,
@@ -5537,12 +5554,41 @@ export class WorkspaceModule {
5537
5554
  initialSyncDone: mount.initialSyncDone,
5538
5555
  currentBranch: currentBranch.name,
5539
5556
  lastMaterializedBranch: mount.lastMaterializedBranchId,
5540
- canMaterialize: this.mountMaterializeBlockReason(store, mount) === null,
5557
+ canMaterialize: this.mountMaterializeBlockReason(store, mount) === null && root?.unavailable === undefined,
5541
5558
  ...(mount.lastAgentActionScan ? { lastAgentActionScan: mount.lastAgentActionScan } : {}),
5559
+ ...(root !== undefined ? { root } : {}),
5542
5560
  };
5543
5561
  }
5544
5562
  return { success: true, data: status };
5545
5563
  }
5564
+ async readView(mount) {
5565
+ const root = resolve(mount.config.path);
5566
+ const rootReal = await realpath(root).catch(() => root);
5567
+ return { view: this.viewOf(mount, this.agreementOrThrow().rootIdentityForRead(mount.config.name)), rootReal };
5568
+ }
5569
+ async rootState(mount) {
5570
+ const read = await this.readView(mount).catch((err) => {
5571
+ if (err instanceof EvidenceUnavailable)
5572
+ return err;
5573
+ throw err;
5574
+ });
5575
+ if (read instanceof EvidenceUnavailable) {
5576
+ const root = resolve(mount.config.path);
5577
+ const there = await locateRoot(this.viewOf(mount, undefined), await realpath(root).catch(() => root));
5578
+ return { now: asShown('identity' in there ? there.identity : null), unavailable: rootUnreadable(read) };
5579
+ }
5580
+ const { view, rootReal } = read;
5581
+ const recorded = view.rootIdentity ?? null;
5582
+ const found = await passRoot({ view, rootReal });
5583
+ if ('unavailable' in found) {
5584
+ const there = await locateRoot({ ...view, rootIdentity: undefined }, rootReal);
5585
+ return { recorded: asShown(recorded), now: asShown('identity' in there ? there.identity : null), unavailable: found.listed };
5586
+ }
5587
+ const note = this.agreementOrThrow().awaitsReopen ? undefined
5588
+ : recorded === null ? mount.config.mode === 'read-only' ? NOTHING_RECORDED_READ_ONLY : NOTHING_RECORDED
5589
+ : recorded.born === undefined && found.identity.born !== undefined && found.bornFixed ? BORN_UNRECORDED : undefined;
5590
+ return { recorded: asShown(recorded), now: asShown(found.identity), ...(note !== undefined ? { note } : {}) };
5591
+ }
5546
5592
  /**
5547
5593
  * What the store holds of a mount that disk lacks, on the selected branch,
5548
5594
  * among the mount's own paths. Status counts the first three as pending:
@@ -6069,7 +6115,7 @@ export class WorkspaceModule {
6069
6115
  return false; // still missing: the pass says so
6070
6116
  const agreement = this.agreementOrThrow();
6071
6117
  const recorded = agreement.rootIdentity(mount.config.name);
6072
- if (recorded && sameRootIdentity(recorded, found.identity))
6118
+ if (recorded && sameRootIdentity(recorded, found.identity, found.bornFixed))
6073
6119
  return false;
6074
6120
  if (recorded)
6075
6121
  agreement.acceptRoot(mount.config.name, found.identity);
@@ -6477,7 +6523,7 @@ function deletedOnDisk(path) {
6477
6523
  function uncheckedFile(shown, note, cause) {
6478
6524
  if (note === DIRECTORY)
6479
6525
  return `${shown} is a directory, not a file; ls ${shown} lists what's in it.`;
6480
- const on = note === ROOT_REPLACED || cause.ownStep === true ? null
6526
+ const on = note === ROOT_REPLACED || note === ROOT_ON_ANOTHER_DEVICE || cause.ownStep === true ? null
6481
6527
  : (note === undefined ? null : wayOn(note, cause, 'read')) ?? 'calling again checks disk again; if the same reason comes back, whoever runs this host can fix it on disk';
6482
6528
  return `${shown} isn't in the workspace, and disk couldn't be checked: ${sentence(note ?? 'not observed')}${on !== null ? ` ${on[0].toUpperCase()}${sentence(on.slice(1))}` : ''}`;
6483
6529
  }