ethagent 1.1.2 → 2.0.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 (268) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +126 -30
  3. package/package.json +7 -2
  4. package/src/app/FirstRun.tsx +190 -146
  5. package/src/app/FirstRunTimeline.tsx +47 -0
  6. package/src/app/input/AppInputProvider.tsx +1 -1
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -1
  8. package/src/chat/ChatBottomPane.tsx +0 -1
  9. package/src/chat/ChatInput.tsx +6 -6
  10. package/src/chat/ChatScreen.tsx +35 -15
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +10 -22
  13. package/src/chat/CopyPicker.tsx +0 -1
  14. package/src/chat/MessageList.tsx +62 -45
  15. package/src/chat/PermissionPrompt.tsx +13 -9
  16. package/src/chat/PlanApprovalView.tsx +3 -3
  17. package/src/chat/ResumeView.tsx +1 -4
  18. package/src/chat/RewindView.tsx +2 -2
  19. package/src/chat/chatInputState.ts +1 -1
  20. package/src/chat/chatScreenUtils.ts +22 -11
  21. package/src/chat/chatSessionState.ts +2 -2
  22. package/src/chat/chatTurnOrchestrator.ts +16 -81
  23. package/src/chat/commands.ts +1 -1
  24. package/src/chat/textCursor.ts +1 -1
  25. package/src/chat/transcriptViewport.ts +2 -7
  26. package/src/cli/ResetConfirmView.tsx +1 -1
  27. package/src/cli/main.tsx +9 -3
  28. package/src/cli/preview.tsx +0 -5
  29. package/src/cli/updateNotice.ts +4 -2
  30. package/src/identity/continuity/editor.ts +7 -107
  31. package/src/identity/continuity/envelope.ts +1048 -40
  32. package/src/identity/continuity/history.ts +4 -4
  33. package/src/identity/continuity/localBackup.ts +249 -0
  34. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  35. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  36. package/src/identity/continuity/privateEdit/files.ts +23 -0
  37. package/src/identity/continuity/privateEdit/types.ts +28 -0
  38. package/src/identity/continuity/privateEdit.ts +10 -298
  39. package/src/identity/continuity/publicSkills.ts +8 -9
  40. package/src/identity/continuity/snapshots.ts +17 -6
  41. package/src/identity/continuity/storage/defaults.ts +111 -0
  42. package/src/identity/continuity/storage/files.ts +72 -0
  43. package/src/identity/continuity/storage/markdown.ts +81 -0
  44. package/src/identity/continuity/storage/paths.ts +24 -0
  45. package/src/identity/continuity/storage/scaffold.ts +124 -0
  46. package/src/identity/continuity/storage/status.ts +86 -0
  47. package/src/identity/continuity/storage/types.ts +27 -0
  48. package/src/identity/continuity/storage.ts +32 -507
  49. package/src/identity/continuity/zipWriter.ts +95 -0
  50. package/src/identity/crypto/backupEnvelope.ts +14 -247
  51. package/src/identity/crypto/eth.ts +7 -7
  52. package/src/identity/ens/agentRecords.ts +96 -0
  53. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  54. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  55. package/src/identity/ens/ensAutomation/names.ts +14 -0
  56. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  57. package/src/identity/ens/ensAutomation/read.ts +114 -0
  58. package/src/identity/ens/ensAutomation/root.ts +63 -0
  59. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  60. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  61. package/src/identity/ens/ensAutomation/types.ts +126 -0
  62. package/src/identity/ens/ensAutomation.ts +29 -0
  63. package/src/identity/ens/ensLookup/client.ts +43 -0
  64. package/src/identity/ens/ensLookup/constants.ts +26 -0
  65. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  66. package/src/identity/ens/ensLookup/names.ts +34 -0
  67. package/src/identity/ens/ensLookup/records.ts +45 -0
  68. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  69. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  70. package/src/identity/ens/ensLookup/types.ts +38 -0
  71. package/src/identity/ens/ensLookup/validation.ts +72 -0
  72. package/src/identity/ens/ensLookup.ts +19 -0
  73. package/src/identity/ens/ensRegistration.ts +199 -0
  74. package/src/identity/ens/resolverDelegation.ts +48 -0
  75. package/src/identity/hub/IdentityHub.tsx +13 -817
  76. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  77. package/src/identity/hub/Routes.tsx +361 -0
  78. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  79. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  80. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  81. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  82. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  83. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  84. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  85. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  86. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  87. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  88. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  89. package/src/identity/hub/effects/create.ts +310 -0
  90. package/src/identity/hub/effects/ens/flows.ts +218 -0
  91. package/src/identity/hub/effects/ens/index.ts +11 -0
  92. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  93. package/src/identity/hub/effects/index.ts +74 -0
  94. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  95. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  96. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  97. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  98. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  99. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  100. package/src/identity/hub/effects/receipts.ts +46 -0
  101. package/src/identity/hub/effects/restore/apply.ts +112 -0
  102. package/src/identity/hub/effects/restore/auth.ts +159 -0
  103. package/src/identity/hub/effects/restore/discover.ts +86 -0
  104. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  105. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  106. package/src/identity/hub/effects/restore/index.ts +22 -0
  107. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  108. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  109. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  110. package/src/identity/hub/effects/restore/shared.ts +91 -0
  111. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  112. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  113. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  114. package/src/identity/hub/effects/shared/sync.ts +190 -0
  115. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  116. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  117. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  118. package/src/identity/hub/effects/types.ts +53 -0
  119. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  120. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  121. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  122. package/src/identity/hub/{screens → flows/continuity}/RecoveryConfirmScreen.tsx +28 -19
  123. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  124. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  125. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  126. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  127. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  128. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  129. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  130. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  131. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  132. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  133. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  134. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  135. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  136. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  137. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  138. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  139. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  140. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  141. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  142. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  143. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  144. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  145. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  146. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  147. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +23 -44
  148. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  149. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  150. package/src/identity/hub/identityHubReducer.ts +164 -99
  151. package/src/identity/hub/model/continuity.ts +94 -0
  152. package/src/identity/hub/model/copy.ts +35 -0
  153. package/src/identity/hub/model/custody.ts +54 -0
  154. package/src/identity/hub/model/ens.ts +49 -0
  155. package/src/identity/hub/model/errors.ts +140 -0
  156. package/src/identity/hub/model/format.ts +15 -0
  157. package/src/identity/hub/model/identity.ts +94 -0
  158. package/src/identity/hub/model/network.ts +32 -0
  159. package/src/identity/hub/model/transfer.ts +57 -0
  160. package/src/identity/hub/operatorWallets.ts +131 -0
  161. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  162. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  165. package/src/identity/hub/reconciliation/index.ts +21 -0
  166. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  167. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  168. package/src/identity/hub/txGuard.ts +51 -0
  169. package/src/identity/hub/types.ts +17 -0
  170. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  171. package/src/identity/hub/useIdentityHubController.ts +396 -0
  172. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  173. package/src/identity/hub/utils.ts +79 -0
  174. package/src/identity/identityCompat.ts +34 -0
  175. package/src/identity/profile/agentIcon.ts +61 -0
  176. package/src/identity/profile/imagePicker.ts +12 -12
  177. package/src/identity/registry/erc8004/abi.ts +14 -0
  178. package/src/identity/registry/erc8004/chains.ts +150 -0
  179. package/src/identity/registry/erc8004/client.ts +11 -0
  180. package/src/identity/registry/erc8004/discovery.ts +511 -0
  181. package/src/identity/registry/erc8004/metadata.ts +335 -0
  182. package/src/identity/registry/erc8004/ownership.ts +121 -0
  183. package/src/identity/registry/erc8004/preflight.ts +123 -0
  184. package/src/identity/registry/erc8004/transactions.ts +77 -0
  185. package/src/identity/registry/erc8004/types.ts +88 -0
  186. package/src/identity/registry/erc8004/uri.ts +59 -0
  187. package/src/identity/registry/erc8004/utils.ts +58 -0
  188. package/src/identity/registry/erc8004.ts +53 -1106
  189. package/src/identity/registry/fieldParsers.ts +28 -0
  190. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  191. package/src/identity/registry/operatorVault/constants.ts +38 -0
  192. package/src/identity/registry/operatorVault/read.ts +246 -0
  193. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  194. package/src/identity/registry/operatorVault.ts +44 -0
  195. package/src/identity/storage/ipfs.ts +26 -24
  196. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  197. package/src/identity/wallet/browserWallet/html.ts +106 -0
  198. package/src/identity/wallet/browserWallet/http.ts +28 -0
  199. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  200. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  201. package/src/identity/wallet/browserWallet/session.ts +325 -0
  202. package/src/identity/wallet/browserWallet/types.ts +192 -0
  203. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  204. package/src/identity/wallet/browserWallet.ts +30 -393
  205. package/src/identity/wallet/page/constants.ts +5 -0
  206. package/src/identity/wallet/page/controller.ts +251 -0
  207. package/src/identity/wallet/page/copy.ts +340 -0
  208. package/src/identity/wallet/page/grainient.ts +278 -0
  209. package/src/identity/wallet/page/html.ts +28 -0
  210. package/src/identity/wallet/page/markup.ts +50 -0
  211. package/src/identity/wallet/page/state.ts +9 -0
  212. package/src/identity/wallet/page/styles/base.ts +259 -0
  213. package/src/identity/wallet/page/styles/components.ts +262 -0
  214. package/src/identity/wallet/page/styles/index.ts +5 -0
  215. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  216. package/src/identity/wallet/page/types.ts +47 -0
  217. package/src/identity/wallet/page/view.ts +535 -0
  218. package/src/identity/wallet/page/walletProvider.ts +70 -0
  219. package/src/identity/wallet/page.tsx +38 -0
  220. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  221. package/src/mcp/manager.ts +0 -1
  222. package/src/models/ModelPicker.tsx +36 -30
  223. package/src/models/catalog.ts +5 -2
  224. package/src/models/huggingface.ts +9 -9
  225. package/src/models/llamacpp.ts +13 -13
  226. package/src/models/modelDisplay.ts +75 -0
  227. package/src/models/modelPickerOptions.ts +16 -3
  228. package/src/models/modelRecommendation.ts +0 -1
  229. package/src/providers/errors.ts +16 -0
  230. package/src/providers/gemini.ts +252 -39
  231. package/src/providers/registry.ts +2 -2
  232. package/src/providers/retry.ts +1 -1
  233. package/src/runtime/sessionMode.ts +1 -1
  234. package/src/runtime/systemPrompt.ts +2 -0
  235. package/src/runtime/toolExecution.ts +18 -22
  236. package/src/runtime/toolIntent.ts +0 -20
  237. package/src/runtime/turn.ts +0 -92
  238. package/src/storage/atomicWrite.ts +4 -1
  239. package/src/storage/config.ts +181 -5
  240. package/src/storage/identity.ts +9 -3
  241. package/src/storage/secrets.ts +2 -2
  242. package/src/tools/bashSafety.ts +8 -0
  243. package/src/tools/changeDirectoryTool.ts +1 -1
  244. package/src/tools/deleteFileTool.ts +4 -4
  245. package/src/tools/editTool.ts +4 -4
  246. package/src/tools/editUtils.ts +5 -5
  247. package/src/tools/privateContinuityEditTool.ts +4 -5
  248. package/src/tools/privateContinuityReadTool.ts +1 -2
  249. package/src/tools/registry.ts +30 -0
  250. package/src/tools/writeFileTool.ts +5 -5
  251. package/src/ui/BrandSplash.tsx +20 -85
  252. package/src/ui/ProgressBar.tsx +3 -5
  253. package/src/ui/Select.tsx +20 -8
  254. package/src/ui/Spinner.tsx +38 -3
  255. package/src/ui/Surface.tsx +2 -2
  256. package/src/ui/TextInput.tsx +63 -20
  257. package/src/ui/theme.ts +7 -34
  258. package/src/utils/openExternal.ts +21 -0
  259. package/src/utils/withRetry.ts +47 -3
  260. package/src/identity/hub/identityHubEffects.ts +0 -937
  261. package/src/identity/hub/identityHubModel.ts +0 -371
  262. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -156
  263. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -146
  264. package/src/identity/hub/screens/IdentitySummary.tsx +0 -106
  265. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  266. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  267. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  268. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -55,12 +55,6 @@ function normalize(event: StreamEvent): ProviderTurnEvent {
55
55
  }
56
56
  }
57
57
 
58
- /**
59
- * MAX_CONTINUATION_NUDGES: if the model stops a turn without emitting any
60
- * tool_use AND the last assistant text signals intent to continue (e.g.
61
- * "now I'll..."), we re-invoke the provider up to this many times with a
62
- * small meta nudge appended.
63
- */
64
58
  export const MAX_CONTINUATION_NUDGES = 3
65
59
 
66
60
  export type ContinuationNudgeReason =
@@ -97,11 +91,6 @@ const PRIVATE_CONTINUITY_REPAIR_NUDGE_TEXT =
97
91
  const REASONING_ONLY_NUDGE_TEXT =
98
92
  'You produced private reasoning but no user-visible answer. Answer the user now in visible text. Do not continue only with reasoning.'
99
93
 
100
- /**
101
- * TurnEvent - events emitted by the runtime turn loop. The UI layer subscribes
102
- * and translates these into Ink rows, notes, permission prompts, and session
103
- * writes. The shape is intentionally trimmed to what ethagent actually uses.
104
- */
105
94
  export type TurnEvent =
106
95
  | { type: 'iteration_start'; index: number }
107
96
  | { type: 'text'; delta: string }
@@ -144,66 +133,21 @@ export type ExecutedToolUse = {
144
133
  cwd: string
145
134
  }
146
135
 
147
- /**
148
- * The runtime loop needs a way to hand pending tool_uses to the host (the UI
149
- * adapter) so the host can:
150
- * - render tool_use / tool_result rows,
151
- * - persist tool_use / tool_result session messages,
152
- * - route permission prompts,
153
- * - and detect cancellation mid-execution.
154
- *
155
- * The host returns what actually happened so the loop can feed tool_results
156
- * back to the provider for the next iteration.
157
- */
158
136
  export type ToolBatchRunner = (
159
137
  pendingToolUses: PendingToolUse[],
160
138
  ) => Promise<{ cancelled: boolean; completedTools: ExecutedToolUse[] }>
161
139
 
162
- /**
163
- * rebuildWorkingMessages: after every tool batch, the host recomputes the
164
- * Message[] it wants to send to the provider. This keeps microcompact,
165
- * system-prompt composition, and file-mention context completely outside the
166
- * loop - the loop only cares about "give me the next prompt window".
167
- */
168
140
  export type RebuildMessages = () => Message[] | Promise<Message[]>
169
141
 
170
142
  export type RuntimeTurnParams = {
171
143
  provider: Provider
172
144
  signal: AbortSignal
173
- /** Initial Message[] to send. */
174
145
  initialMessages: Message[]
175
- /**
176
- * Called after every tool execution round to rebuild the Message[] for the
177
- * next provider call. The host is responsible for microcompact, system
178
- * prompt, and any mention context - the loop is deliberately dumb here.
179
- */
180
146
  rebuildMessages: RebuildMessages
181
147
  runToolBatch: ToolBatchRunner
182
- /** Upper bound on continuation nudges per turn. Defaults to MAX_CONTINUATION_NUDGES. */
183
148
  maxContinuationNudges?: number
184
149
  }
185
150
 
186
- /**
187
- * runRuntimeTurn - the one and only turn loop.
188
- *
189
- * Shape:
190
- * 1. Stream the provider.
191
- * 2. Collect tool_use blocks from native `tool_use_stop` events.
192
- * 3. If the model emitted tool_uses: execute them, feed results back, loop.
193
- * 4. If it didn't: check if the last assistant text signals intent to
194
- * continue ("now I'll..."). If yes and we're under the cap, append a
195
- * soft meta nudge and loop. Otherwise exit.
196
- *
197
- * Intentionally absent:
198
- * - No provider-family branching (no isLocalProvider specialization).
199
- * - No broad regex fallback tool parsing. A narrow local-model
200
- * compatibility parser handles standalone JSON tool payloads only.
201
- * - No duplicate-tool-call suppression. The model is allowed to repeat.
202
- * - No broad forced-repair retries on tool input validation errors - errors
203
- * go back to the model as tool_result(is_error). Private continuity gets
204
- * one narrow local-model repair nudge because bad JSON there is common and
205
- * user-visible.
206
- */
207
151
  export async function* runRuntimeTurn(
208
152
  params: RuntimeTurnParams,
209
153
  ): AsyncGenerator<TurnEvent, void, void> {
@@ -301,8 +245,6 @@ export async function* runRuntimeTurn(
301
245
  const parsedToolUses = parseLocalModelTextToolUses(provider, assistantText, iterationIndex - 1)
302
246
  if (parsedToolUses.length > 0) {
303
247
  pendingToolUses.push(...parsedToolUses)
304
- // Signal the orchestrator to discard any streamed assistant text
305
- // rows that contained the JSON blob - they should not be persisted.
306
248
  yield { type: 'local_tool_recovery' }
307
249
  for (const parsedToolUse of parsedToolUses) {
308
250
  yield {
@@ -371,10 +313,6 @@ export async function* runRuntimeTurn(
371
313
  attempt: continuationNudges,
372
314
  reason: 'tool_state_claim',
373
315
  }
374
- // Rebuild from scratch, inject a correction context message to
375
- // demote prior unsupported assistant claims, then append the nudge.
376
- // This prevents the model from reinforcing its own false claims
377
- // on subsequent iterations within the same turn.
378
316
  workingMessages = [
379
317
  ...await rebuildMessages(),
380
318
  {
@@ -397,7 +335,6 @@ export async function* runRuntimeTurn(
397
335
  }
398
336
  }
399
337
 
400
- // No tool work: model decided this turn is over (modulo continuation nudge).
401
338
  if (pendingToolUses.length === 0) {
402
339
  if (!assistantText && thinkingSeen) {
403
340
  if (continuationNudges < maxContinuationNudges) {
@@ -423,9 +360,6 @@ export async function* runRuntimeTurn(
423
360
 
424
361
  const nudge = nextNudge(provider, assistantText)
425
362
  if (assistantText && continuationNudges < maxContinuationNudges && nudge) {
426
- // After a tool batch, the model's summary text often accidentally
427
- // matches the continuation-intent heuristic ("I've updated...").
428
- // Commit the text and end the turn instead of nudging again.
429
363
  if (hadToolsLastRound && nudge.reason === 'continuation') {
430
364
  yield { type: 'assistant_message_committed', text: assistantText }
431
365
  yield doneEvent(true, stopReason)
@@ -461,10 +395,6 @@ export async function* runRuntimeTurn(
461
395
  return
462
396
  }
463
397
 
464
- // Tool work: hand the batch to the host. The host renders rows, persists
465
- // the tool_use/tool_result session messages, and routes permission prompts.
466
- // We then emit tool_executed events so UI adapters that care (e.g., tests)
467
- // can observe each completed tool before we loop back to the provider.
468
398
  const batch = await runToolBatch(pendingToolUses)
469
399
  for (const completed of batch.completedTools) {
470
400
  toolEvidenceThisTurn.push({
@@ -659,11 +589,6 @@ function parseTextToolPayloads(payload: string): Array<{ name: string; input: Re
659
589
  return normalizeParsedToolPayloads(parsed)
660
590
  }
661
591
 
662
- function parseTextToolPayload(payload: string): { name: string; input: Record<string, unknown> } | null {
663
- const calls = parseTextToolPayloads(payload)
664
- return calls.length === 1 ? calls[0]! : null
665
- }
666
-
667
592
  function normalizeParsedToolPayloads(value: unknown): Array<{ name: string; input: Record<string, unknown> }> {
668
593
  if (Array.isArray(value)) {
669
594
  return value.flatMap(normalizeParsedToolPayloads)
@@ -687,11 +612,6 @@ function normalizeParsedToolPayloads(value: unknown): Array<{ name: string; inpu
687
612
  return call ? [call] : []
688
613
  }
689
614
 
690
- function normalizeParsedToolPayload(value: unknown): { name: string; input: Record<string, unknown> } | null {
691
- const calls = normalizeParsedToolPayloads(value)
692
- return calls.length === 1 ? calls[0]! : null
693
- }
694
-
695
615
  function normalizeNameAndInput(
696
616
  name: unknown,
697
617
  rawInput: unknown,
@@ -808,18 +728,6 @@ export function looksLikeToolDelegationText(text: string): boolean {
808
728
  return askUser || selfIntent || commandForm || asksForOutput
809
729
  }
810
730
 
811
- /**
812
- * looksLikeContinuationIntent - heuristic for continuation nudge detection.
813
- *
814
- * Two rules:
815
- * 1. If the text contains an explicit completion marker ("done", "all set",
816
- * "let me know if"), never nudge.
817
- * 2. Otherwise, nudge iff at least one action-intent pattern matches
818
- * ("now I'll edit", "let me create", "time to run", etc.).
819
- *
820
- * Deliberately narrow. We never rewrite the model's output; we only decide
821
- * whether to append a short meta user message and re-stream.
822
- */
823
731
  export function looksLikeContinuationIntent(text: string): boolean {
824
732
  const lower = text.toLowerCase()
825
733
 
@@ -1,8 +1,11 @@
1
+ import crypto from 'node:crypto'
1
2
  import fs from 'node:fs/promises'
2
3
 
3
4
  const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
4
5
  const RETRY_DELAYS_MS = [20, 60, 120]
5
6
 
7
+ let tempCounter = 0
8
+
6
9
  type WriteOptions = {
7
10
  mode?: number
8
11
  }
@@ -12,7 +15,7 @@ export async function atomicWriteText(
12
15
  data: string,
13
16
  options: WriteOptions = {},
14
17
  ): Promise<void> {
15
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
18
+ const tmp = `${file}.${process.pid}.${Date.now()}.${(tempCounter = (tempCounter + 1) >>> 0)}.${crypto.randomBytes(4).toString('hex')}.tmp`
16
19
  const mode = options.mode ?? 0o600
17
20
 
18
21
  await fs.writeFile(tmp, data, { encoding: 'utf8', mode })
@@ -8,7 +8,7 @@ export const PROVIDERS = ['llamacpp', 'openai', 'anthropic', 'gemini'] as const
8
8
  export type ProviderId = (typeof PROVIDERS)[number]
9
9
  const LEGACY_PROVIDERS = ['ollama', ...PROVIDERS] as const
10
10
 
11
- export const SELECTABLE_NETWORKS = ['mainnet', 'arbitrum', 'base', 'optimism', 'polygon'] as const
11
+ export const SELECTABLE_NETWORKS = ['mainnet', 'base'] as const
12
12
  export type SelectableNetwork = (typeof SELECTABLE_NETWORKS)[number]
13
13
 
14
14
  const IdentitySchema = z.object({
@@ -16,6 +16,7 @@ const IdentitySchema = z.object({
16
16
  createdAt: z.string(),
17
17
  source: z.enum(['local-key', 'erc8004']).optional(),
18
18
  ownerAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
19
+ connectedWallet: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
19
20
  chainId: z.number().int().positive().optional(),
20
21
  rpcUrl: z.string().url().optional(),
21
22
  identityRegistryAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
@@ -37,6 +38,14 @@ const IdentitySchema = z.object({
37
38
  agentUri: z.string().min(1).optional(),
38
39
  metadataCid: z.string().min(1).optional(),
39
40
  txHash: z.string().regex(/^0x[a-fA-F0-9]+$/).optional(),
41
+ transferSnapshot: z.object({
42
+ kind: z.literal('dual-wallet'),
43
+ senderAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
44
+ receiverAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
45
+ receiverHandle: z.string().min(1).optional(),
46
+ slotCount: z.number().int().positive(),
47
+ createdAt: z.string().optional(),
48
+ }).optional(),
40
49
  pastBackups: z.array(z.object({
41
50
  cid: z.string().min(1),
42
51
  createdAt: z.string(),
@@ -48,6 +57,22 @@ const IdentitySchema = z.object({
48
57
  updatedAt: z.string().optional(),
49
58
  status: z.enum(['pinned', 'failed', 'unknown']).optional(),
50
59
  }).optional(),
60
+ pendingTx: z.object({
61
+ hash: z.string().regex(/^0x[a-fA-F0-9]+$/),
62
+ kind: z.enum([
63
+ 'register',
64
+ 'rebackup-uri',
65
+ 'rebackup-uri-vault',
66
+ 'token-transfer',
67
+ 'public-profile',
68
+ 'vault-deploy',
69
+ 'vault-deposit',
70
+ 'vault-unwrap',
71
+ 'vault-withdraw',
72
+ ]),
73
+ chainId: z.number().int().positive(),
74
+ submittedAt: z.string(),
75
+ }).optional(),
51
76
  })
52
77
 
53
78
  const ConfigSchema = z.object({
@@ -62,8 +87,13 @@ const ConfigSchema = z.object({
62
87
  rpcUrl: z.string().url(),
63
88
  identityRegistryAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
64
89
  fromBlock: z.string().regex(/^\d+$/).optional(),
90
+ operatorVaults: z.record(
91
+ z.string().regex(/^\d+$/),
92
+ z.string().regex(/^0x[a-fA-F0-9]{40}$/),
93
+ ).optional(),
65
94
  }).optional(),
66
95
  selectedNetwork: z.enum(SELECTABLE_NETWORKS).optional(),
96
+ configVersion: z.number().int().nonnegative().optional(),
67
97
  })
68
98
 
69
99
  const LEGACY_OLLAMA_BASE_URL = 'http://localhost:11434/v1'
@@ -74,6 +104,7 @@ const LegacyConfigSchema = ConfigSchema.extend({
74
104
  type LegacyConfig = z.infer<typeof LegacyConfigSchema>
75
105
 
76
106
  export type EthagentIdentity = z.infer<typeof IdentitySchema>
107
+ export type TransferSnapshotMetadata = NonNullable<NonNullable<EthagentIdentity['backup']>['transferSnapshot']>
77
108
 
78
109
  export type EthagentConfig = z.infer<typeof ConfigSchema>
79
110
 
@@ -112,11 +143,143 @@ export async function loadConfig(): Promise<EthagentConfig | null> {
112
143
 
113
144
  export async function saveConfig(config: EthagentConfig): Promise<void> {
114
145
  await ensureConfigDir()
115
- const validated = ConfigSchema.parse(normalizeConfig(config))
146
+ const bumped: EthagentConfig = {
147
+ ...normalizeConfig(config),
148
+ configVersion: (config.configVersion ?? 0) + 1,
149
+ }
150
+ const validated = ConfigSchema.parse(bumped)
116
151
  const file = getConfigPath()
117
152
  await atomicWriteText(file, JSON.stringify(validated, null, 2) + '\n')
118
153
  }
119
154
 
155
+ export class ConfigVersionStaleError extends Error {
156
+ readonly baseVersion: number | undefined
157
+ readonly currentVersion: number | undefined
158
+ constructor(baseVersion: number | undefined, currentVersion: number | undefined) {
159
+ super('Config write conflict detected: another writer beat this update. Retry to merge.')
160
+ this.name = 'ConfigVersionStaleError'
161
+ this.baseVersion = baseVersion
162
+ this.currentVersion = currentVersion
163
+ }
164
+ }
165
+
166
+ export async function saveConfigGuarded(
167
+ prev: EthagentConfig | null,
168
+ next: EthagentConfig,
169
+ ): Promise<EthagentConfig> {
170
+ const current = await loadConfig()
171
+ const currentVersion = current?.configVersion
172
+ const baseVersion = prev?.configVersion
173
+ if (currentVersion !== baseVersion) {
174
+ throw new ConfigVersionStaleError(baseVersion, currentVersion)
175
+ }
176
+ const toWrite: EthagentConfig = { ...next, configVersion: currentVersion ?? 0 }
177
+ await saveConfig(toWrite)
178
+ return { ...toWrite, configVersion: (currentVersion ?? 0) + 1 }
179
+ }
180
+
181
+ export async function saveConfigWithMerge(
182
+ applyPatch: (current: EthagentConfig | null) => EthagentConfig | Promise<EthagentConfig>,
183
+ attempts: number = 3,
184
+ ): Promise<EthagentConfig> {
185
+ let lastErr: ConfigVersionStaleError | undefined
186
+ for (let attempt = 0; attempt < attempts; attempt++) {
187
+ const current = await loadConfig()
188
+ const next = await applyPatch(current)
189
+ try {
190
+ return await saveConfigGuarded(current, next)
191
+ } catch (err: unknown) {
192
+ if (!(err instanceof ConfigVersionStaleError)) throw err
193
+ lastErr = err
194
+ }
195
+ }
196
+ throw lastErr ?? new ConfigVersionStaleError(undefined, undefined)
197
+ }
198
+
199
+ export function getConfiguredOperatorVaultAddress(
200
+ config: EthagentConfig | null | undefined,
201
+ chainId: number,
202
+ ): string | undefined {
203
+ return config?.erc8004?.operatorVaults?.[String(chainId)]
204
+ }
205
+
206
+ export function buildSeedConfigForIdentity(args: {
207
+ identity: EthagentIdentity
208
+ chainId: number
209
+ rpcUrl: string
210
+ identityRegistryAddress: string
211
+ }): EthagentConfig {
212
+ return {
213
+ version: 1,
214
+ provider: 'llamacpp',
215
+ model: defaultModelFor('llamacpp'),
216
+ firstRunAt: new Date().toISOString(),
217
+ identity: { ...args.identity, source: 'erc8004' },
218
+ erc8004: {
219
+ chainId: args.chainId,
220
+ rpcUrl: args.rpcUrl,
221
+ identityRegistryAddress: args.identityRegistryAddress,
222
+ },
223
+ }
224
+ }
225
+
226
+ export function setConfiguredOperatorVaultAddress(
227
+ config: EthagentConfig,
228
+ chainId: number,
229
+ vaultAddress: string,
230
+ ): EthagentConfig {
231
+ if (!config.erc8004) {
232
+ throw new Error('Cannot record operator delegation vault address: erc8004 registry config is not set')
233
+ }
234
+ return {
235
+ ...config,
236
+ erc8004: {
237
+ ...config.erc8004,
238
+ operatorVaults: {
239
+ ...(config.erc8004.operatorVaults ?? {}),
240
+ [String(chainId)]: vaultAddress,
241
+ },
242
+ },
243
+ }
244
+ }
245
+
246
+ export type PendingTxRecord = NonNullable<EthagentIdentity['pendingTx']>
247
+ export type PendingTxKind = PendingTxRecord['kind']
248
+
249
+ export async function recordPendingTx(record: PendingTxRecord): Promise<EthagentConfig | null> {
250
+ return savePendingTxMutation(identity => ({ ...identity, pendingTx: record }))
251
+ }
252
+
253
+ export async function clearPendingTx(): Promise<EthagentConfig | null> {
254
+ return savePendingTxMutation(identity => {
255
+ if (!identity.pendingTx) return null
256
+ const { pendingTx: _drop, ...identityRest } = identity
257
+ return identityRest
258
+ })
259
+ }
260
+
261
+ async function savePendingTxMutation(
262
+ mutate: (identity: NonNullable<EthagentConfig['identity']>) => NonNullable<EthagentConfig['identity']> | null,
263
+ ): Promise<EthagentConfig | null> {
264
+ for (let attempt = 0; attempt < 2; attempt++) {
265
+ const config = await loadConfig()
266
+ if (!config?.identity) return config
267
+ const mutatedIdentity = mutate(config.identity)
268
+ if (!mutatedIdentity) return config
269
+ const next: EthagentConfig = { ...config, identity: mutatedIdentity }
270
+ try {
271
+ return await saveConfigGuarded(config, next)
272
+ } catch (err: unknown) {
273
+ if (!(err instanceof ConfigVersionStaleError)) throw err
274
+ if (attempt === 1) {
275
+ console.warn('[ethagent] pending-tx config write skipped: cross-tab conflict persisted after retry')
276
+ return null
277
+ }
278
+ }
279
+ }
280
+ return null
281
+ }
282
+
120
283
  export async function deleteConfig(): Promise<void> {
121
284
  try {
122
285
  await fs.unlink(getConfigPath())
@@ -148,9 +311,22 @@ export function localProviderBaseUrlFor(provider: LocalProviderId, baseUrl?: str
148
311
  }
149
312
 
150
313
  export function normalizeConfig(config: EthagentConfig): EthagentConfig {
151
- if (config.provider !== 'llamacpp') return config
152
- const baseUrl = localProviderBaseUrlFor(config.provider, config.baseUrl)
153
- return config.baseUrl === baseUrl ? config : { ...config, baseUrl }
314
+ let next = config
315
+ if (next.provider === 'llamacpp') {
316
+ const baseUrl = localProviderBaseUrlFor(next.provider, next.baseUrl)
317
+ if (next.baseUrl !== baseUrl) next = { ...next, baseUrl }
318
+ }
319
+ if (!next.erc8004 && next.identity?.chainId && next.identity.identityRegistryAddress && next.identity.rpcUrl) {
320
+ next = {
321
+ ...next,
322
+ erc8004: {
323
+ chainId: next.identity.chainId,
324
+ rpcUrl: next.identity.rpcUrl,
325
+ identityRegistryAddress: next.identity.identityRegistryAddress,
326
+ },
327
+ }
328
+ }
329
+ return next
154
330
  }
155
331
 
156
332
  function migrateLegacyConfig(config: LegacyConfig): EthagentConfig {
@@ -5,7 +5,6 @@ import {
5
5
  type EthagentIdentity,
6
6
  } from './config.js'
7
7
  import {
8
- getSecret,
9
8
  rmSecret,
10
9
  hasSecret,
11
10
  whichBackend,
@@ -59,7 +58,7 @@ export async function setTokenIdentity(
59
58
  identity: EthagentIdentity,
60
59
  ): Promise<EthagentConfig> {
61
60
  if (!identity.address || !identity.agentId || !identity.agentUri || !identity.ownerAddress) {
62
- throw new Error('token identity is missing ERC-8004 metadata')
61
+ throw new Error('Token identity is missing ERC-8004 metadata')
63
62
  }
64
63
  const next: EthagentConfig = {
65
64
  ...config,
@@ -68,6 +67,13 @@ export async function setTokenIdentity(
68
67
  source: 'erc8004',
69
68
  },
70
69
  }
70
+ if (!next.erc8004 && identity.chainId && identity.rpcUrl && identity.identityRegistryAddress) {
71
+ next.erc8004 = {
72
+ chainId: identity.chainId,
73
+ rpcUrl: identity.rpcUrl,
74
+ identityRegistryAddress: identity.identityRegistryAddress,
75
+ }
76
+ }
71
77
  await saveConfig(next)
72
78
  return next
73
79
  }
@@ -76,7 +82,7 @@ export async function updateIdentityBackup(
76
82
  config: EthagentConfig,
77
83
  backup: NonNullable<EthagentIdentity['backup']>,
78
84
  ): Promise<EthagentConfig> {
79
- if (!config.identity) throw new Error('no identity set')
85
+ if (!config.identity) throw new Error('No identity set')
80
86
  const next: EthagentConfig = {
81
87
  ...config,
82
88
  identity: {
@@ -24,7 +24,7 @@ async function loadKeytar(): Promise<Keytar | null> {
24
24
  if (typeof api.getPassword !== 'function'
25
25
  || typeof api.setPassword !== 'function'
26
26
  || typeof api.deletePassword !== 'function') {
27
- throw new Error('keytar module shape unexpected')
27
+ throw new Error('Keytar module shape unexpected')
28
28
  }
29
29
  await api.getPassword(KEYTAR_SERVICE, '__ethagent_probe__')
30
30
  keytarCache = api
@@ -130,7 +130,7 @@ export async function getSecret(account: string): Promise<string | null> {
130
130
 
131
131
  export async function setSecret(account: string, value: string): Promise<KeyBackend> {
132
132
  const trimmed = value.trim()
133
- if (!trimmed) throw new Error('secret value is empty')
133
+ if (!trimmed) throw new Error('Secret value is empty')
134
134
  const keytar = await loadKeytar()
135
135
  if (keytar) {
136
136
  await keytar.setPassword(KEYTAR_SERVICE, account, trimmed)
@@ -132,6 +132,14 @@ export function validateBashCommandInput(command: string): string | undefined {
132
132
  return `command must be an actual shell command, not an ethagent tool name. ${nativeToolMessage}`
133
133
  }
134
134
 
135
+ if (normalizedFirstToken === 'echo' || normalizedFirstToken === 'printf') {
136
+ const rest = trimmed.slice(firstToken.length)
137
+ const hasShellMeta = /[|&;<>`$]/.test(rest)
138
+ if (!hasShellMeta) {
139
+ return `do not use ${normalizedFirstToken} to emit conversational text; reply directly in your assistant message instead.`
140
+ }
141
+ }
142
+
135
143
  if (
136
144
  /\b(you can|you should|you need|run the following command|written in|under the|to run(?: the game)?|copy and paste|save (?:it|this))/i.test(trimmed)
137
145
  ) {
@@ -56,7 +56,7 @@ function resolveTargetDirectory(workspaceRoot: string, requestedPath: string): s
56
56
  function resolveDirectoryIntent(input: string, workspaceRoot: string): string {
57
57
  const normalized = normalizeIntentInput(input)
58
58
  if (!normalized) {
59
- throw new Error('missing directory path')
59
+ throw new Error('Missing directory path')
60
60
  }
61
61
 
62
62
  if (looksLikeConcretePath(normalized)) {
@@ -70,7 +70,7 @@ async function prepareDelete(input: z.infer<typeof schema>, context: { workspace
70
70
  const fullPath = resolveWorkspacePath(context.workspaceRoot, input.path)
71
71
  const stats = await fs.stat(fullPath)
72
72
  if (stats.isDirectory()) {
73
- throw new Error('delete_file path points to a directory; provide a file path')
73
+ throw new Error('Tool delete_file path points to a directory; provide a file path')
74
74
  }
75
75
  const before = await fs.readFile(fullPath, 'utf8')
76
76
  return {
@@ -83,13 +83,13 @@ async function prepareDelete(input: z.infer<typeof schema>, context: { workspace
83
83
  function assertSafeDeletePath(requestedPath: string): void {
84
84
  const trimmed = requestedPath.trim()
85
85
  if (trimmed !== requestedPath || trimmed.length === 0) {
86
- throw new Error('delete_file path must be a clean workspace-relative file path')
86
+ throw new Error('Tool delete_file path must be a clean workspace-relative file path')
87
87
  }
88
88
  if (/[|;&<>`]/.test(trimmed)) {
89
- throw new Error('delete_file path must not contain shell operators')
89
+ throw new Error('Tool delete_file path must not contain shell operators')
90
90
  }
91
91
  if (/^(?:rm|del|erase|rmdir|remove-item|mkdir|type|cat|echo|copy|move|mv|cp)\b/i.test(trimmed)) {
92
- throw new Error('delete_file path looks like a shell command; pass only the file path')
92
+ throw new Error('Tool delete_file path looks like a shell command; pass only the file path')
93
93
  }
94
94
  }
95
95
 
@@ -114,7 +114,7 @@ async function assertEditableFileTarget(fullPath: string): Promise<void> {
114
114
  try {
115
115
  const stats = await fs.stat(fullPath)
116
116
  if (stats.isDirectory()) {
117
- throw new Error('edit_file path points to a directory; provide a file path')
117
+ throw new Error('Tool edit_file path points to a directory; provide a file path')
118
118
  }
119
119
  } catch (error: unknown) {
120
120
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
@@ -125,15 +125,15 @@ async function assertEditableFileTarget(fullPath: string): Promise<void> {
125
125
  function assertSafeEditPath(requestedPath: string): void {
126
126
  const trimmed = requestedPath.trim()
127
127
  if (trimmed !== requestedPath || trimmed.length === 0) {
128
- throw new Error('edit_file path must be a clean workspace-relative file path')
128
+ throw new Error('Tool edit_file path must be a clean workspace-relative file path')
129
129
  }
130
130
 
131
131
  if (/[|;&<>`]/.test(trimmed)) {
132
- throw new Error('edit_file path must not contain shell operators')
132
+ throw new Error('Tool edit_file path must not contain shell operators')
133
133
  }
134
134
 
135
135
  if (/^(?:rm|del|erase|rmdir|remove-item|mkdir|type|cat|echo|copy|move|mv|cp)\b/i.test(trimmed)) {
136
- throw new Error('edit_file path looks like a shell command; pass only the file path')
136
+ throw new Error('Tool edit_file path looks like a shell command; pass only the file path')
137
137
  }
138
138
  }
139
139
 
@@ -21,7 +21,7 @@ export function applyRequestedEdit(
21
21
  ): AppliedEdit {
22
22
  if (!oldText) {
23
23
  if (newText.length === 0) {
24
- throw new Error('edit_file newText is empty; empty whole-file writes are not valid unless replacing a specific oldText range')
24
+ throw new Error('Field newText is empty; empty whole-file writes are not valid unless replacing a specific oldText range')
25
25
  }
26
26
  return {
27
27
  before,
@@ -34,7 +34,7 @@ export function applyRequestedEdit(
34
34
 
35
35
  if (replaceAll) {
36
36
  const matchCount = countOccurrences(before, oldText)
37
- if (matchCount === 0) throw new Error('oldText was not found in the file')
37
+ if (matchCount === 0) throw new Error('Field oldText was not found in the file')
38
38
  return {
39
39
  before,
40
40
  after: before.replaceAll(oldText, () => newText),
@@ -45,9 +45,9 @@ export function applyRequestedEdit(
45
45
  }
46
46
 
47
47
  const actualOldText = findUniqueEditableMatch(before, oldText)
48
- if (!actualOldText) throw new Error('oldText was not found in the file')
48
+ if (!actualOldText) throw new Error('Field oldText was not found in the file')
49
49
  if (countOccurrences(before, actualOldText) > 1) {
50
- throw new Error('oldText matched multiple locations; provide more context or use replaceAll')
50
+ throw new Error('Field oldText matched multiple locations; provide more context or use replaceAll')
51
51
  }
52
52
 
53
53
  const adjustedNewText = preserveQuoteStyle(oldText, actualOldText, newText)
@@ -62,7 +62,7 @@ export function applyRequestedEdit(
62
62
 
63
63
  function replaceSingleOccurrence(content: string, search: string, replace: string): string {
64
64
  const index = content.indexOf(search)
65
- if (index === -1) throw new Error('oldText was not found in the file')
65
+ if (index === -1) throw new Error('Field oldText was not found in the file')
66
66
  return `${content.slice(0, index)}${replace}${content.slice(index + search.length)}`
67
67
  }
68
68
 
@@ -82,7 +82,7 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
82
82
  'For persona or standing behavior call exactly: {"file":"SOUL.md","appendToSection":"Persona","appendText":"- Persona or standing behavior note."}.',
83
83
  'Prefer appendToSection+appendText to build on an existing scaffold section; use oldText+newText only for targeted replacement after exact text is known.',
84
84
  'Whole-file replacement is disabled for private continuity.',
85
- 'Approved private continuity edits are not managed by /rewind; the previous version is saved to private identity history before writing.',
85
+ 'Approved private continuity edits are not managed by /rewind.',
86
86
  ].join(' '),
87
87
  inputSchema: schema,
88
88
  inputSchemaJson: {
@@ -108,7 +108,7 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
108
108
  path: prepared.fullPath,
109
109
  relativePath: prepared.relativePath,
110
110
  directoryPath: prepared.directoryPath,
111
- title: 'approve private continuity edit?',
111
+ title: 'Approve Private Continuity Edit?',
112
112
  subtitle: prepared.fullPath,
113
113
  file: prepared.file,
114
114
  before: prepared.previewBefore,
@@ -132,7 +132,6 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
132
132
  previousFiles,
133
133
  previousPublicSkills,
134
134
  changeSummary: prepared.changeSummary,
135
- createdAt: new Date().toISOString(),
136
135
  sessionId: context.checkpoint?.sessionId,
137
136
  turnId: context.checkpoint?.turnId,
138
137
  promptSnippet: context.checkpoint?.promptSnippet,
@@ -153,9 +152,9 @@ function formatPrivateContinuityEditResult(file: 'SOUL.md' | 'MEMORY.md', fullPa
153
152
  '',
154
153
  `- File: \`identity-vault/${file}\``,
155
154
  `- Review file: \`${fullPath}\``,
156
- '- Open: Identity Hub > Memory and Persona',
157
- '- Save: Identity Hub > Recovery > Save Snapshot Now',
158
155
  '- History: previous version saved to private identity history; `/rewind` does not restore identity continuity',
156
+ '- Open: Identity Hub > Soul, Memory, and Skills',
157
+ '- Save: Identity Hub > Recovery > Save Snapshot Now',
159
158
  ].join('\n')
160
159
  }
161
160