@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -13,7 +13,8 @@
13
13
  * 文本类(md/txt/json/jsonl/csv/log/yaml/yml)——本工具是「产物查看」,不是通用文件浏览器。
14
14
  */
15
15
 
16
- import { readdir, readFile, stat } from 'node:fs/promises'
16
+ import { createHash } from 'node:crypto'
17
+ import { readdir, readFile, realpath, stat } from 'node:fs/promises'
17
18
  import { existsSync } from 'node:fs'
18
19
  import { isAbsolute, join, resolve, sep } from 'node:path'
19
20
 
@@ -31,11 +32,14 @@ export interface ArtifactEntry {
31
32
 
32
33
  export interface ArtifactReadView {
33
34
  ok: true
35
+ source: 'state-root' | 'cwd'
34
36
  path: string
35
37
  offset: number
36
38
  lines: Array<{ number: number; text: string }>
37
39
  totalLines: number
38
40
  lang?: string
41
+ /** Short content fingerprint shown by the Web Client to distinguish refreshes. */
42
+ version: string
39
43
  content: string
40
44
  windowed: boolean
41
45
  }
@@ -100,6 +104,8 @@ async function listDeliverableFiles(root: string, limit: number): Promise<Artifa
100
104
  const p = join(dir, n)
101
105
  const st = await stat(p).catch(() => null)
102
106
  if (!st?.isFile()) continue
107
+ const canonical = await realpath(p).catch(() => null)
108
+ if (!canonical || !underRoot(canonical, root) || hasDeniedSegment(canonical)) continue
103
109
  entries.push({
104
110
  source: 'async-run',
105
111
  id: n.replace(/\.deliverable\.md$/, ''),
@@ -127,6 +133,8 @@ async function listCwdMarkdown(cwd: string, limit: number): Promise<ArtifactEntr
127
133
  const p = join(cwd, d.name)
128
134
  const st = await stat(p).catch(() => null)
129
135
  if (!st) continue
136
+ const canonical = await realpath(p).catch(() => null)
137
+ if (!canonical || !underRoot(canonical, cwd) || hasDeniedSegment(canonical)) continue
130
138
  entries.push({
131
139
  source: 'cwd-file',
132
140
  id: d.name,
@@ -147,10 +155,12 @@ export async function listArtifacts(opts: {
147
155
  const limit = Math.max(1, Math.min(opts.limit ?? 20, MAX_LIST))
148
156
  const root = rootOf(opts.stateRoot)
149
157
  const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd()
158
+ const canonicalRoot = await realpath(root).catch(() => root)
159
+ const canonicalCwd = await realpath(cwd).catch(() => cwd)
150
160
 
151
161
  const seenPath = new Set<string>()
152
162
  const merged: ArtifactEntry[] = []
153
- for (const e of [...listRunsFromLedger(root, 'local', limit), ...(await listDeliverableFiles(root, limit)), ...(await listCwdMarkdown(cwd, limit))]) {
163
+ for (const e of [...listRunsFromLedger(canonicalRoot, 'local', limit), ...(await listDeliverableFiles(canonicalRoot, limit)), ...(await listCwdMarkdown(canonicalCwd, limit))]) {
154
164
  if (seenPath.has(e.path)) continue
155
165
  seenPath.add(e.path)
156
166
  merged.push(e)
@@ -158,7 +168,7 @@ export async function listArtifacts(opts: {
158
168
  merged.sort((a, b) => String(b.updated ?? '').localeCompare(String(a.updated ?? '')))
159
169
 
160
170
  const total = merged.length
161
- return { artifacts: merged.slice(0, limit), total, truncated: total > limit, roots: [root, cwd] }
171
+ return { artifacts: merged.slice(0, limit), total, truncated: total > limit, roots: [canonicalRoot, canonicalCwd] }
162
172
  }
163
173
 
164
174
  /**
@@ -177,6 +187,8 @@ export async function readArtifact(opts: {
177
187
  }): Promise<ArtifactReadView | { ok: false; error: string; hint?: string }> {
178
188
  const root = rootOf(opts.stateRoot)
179
189
  const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd()
190
+ const canonicalRoot = await realpath(root).catch(() => root)
191
+ const canonicalCwd = await realpath(cwd).catch(() => cwd)
180
192
  const raw = String(opts.path ?? '').trim()
181
193
  if (!raw) return { ok: false, error: 'path 必填(来自 gotry_artifacts_list 的 path,或异步工单 id)' }
182
194
 
@@ -194,7 +206,21 @@ export async function readArtifact(opts: {
194
206
  filePath = asyncDeliverablePath(root, raw)
195
207
  } else {
196
208
  const p = asyncDeliverablePath(root, raw)
197
- if (existsSync(p)) { filePath = p; text = await readFile(p, 'utf-8') }
209
+ const canonical = await realpath(p).catch(() => null)
210
+ if (canonical) {
211
+ if (!underRoot(canonical, canonicalRoot) || hasDeniedSegment(canonical)) {
212
+ return { ok: false, error: `路径越界:${raw}`, hint: `只读 ${root} 与 dsh 工作目录内的文本产物` }
213
+ }
214
+ const ext = canonical.slice(canonical.lastIndexOf('.') + 1).toLowerCase()
215
+ if (!TEXT_EXT_LANG[ext]) {
216
+ return { ok: false, error: `不支持的文件类型 .${ext}`, hint: `白名单:${Object.keys(TEXT_EXT_LANG).join('/')}` }
217
+ }
218
+ const st = await stat(canonical).catch(() => null)
219
+ if (!st?.isFile()) return { ok: false, error: `文件不存在:${raw}`, hint: '先 gotry_artifacts_list 看在册产物' }
220
+ if (st.size > MAX_BYTES) return { ok: false, error: `文件过大(${st.size} bytes > ${MAX_BYTES})` }
221
+ filePath = canonical
222
+ text = await readFile(canonical, 'utf-8')
223
+ }
198
224
  }
199
225
  if (text === null) return { ok: false, error: `工单 ${raw} 无 deliverable(未交付或不存在)`, hint: '先 gotry_artifacts_list 看在册产物' }
200
226
  }
@@ -202,19 +228,28 @@ export async function readArtifact(opts: {
202
228
  // 2) 路径形态:目录白名单 + 扩展名白名单
203
229
  if (text === null) {
204
230
  const candidates = isAbsolute(raw) ? [resolve(raw)] : [resolve(cwd, raw), resolve(root, raw), resolve(root, 'gotry-state', 'async', raw)]
205
- const allowed = candidates.find(p => (underRoot(p, cwd) || underRoot(p, root)) && !hasDeniedSegment(p))
231
+ const inScope = (p: string) => (underRoot(p, cwd) || underRoot(p, root)) && !hasDeniedSegment(p)
232
+ // Prefer an existing candidate so a cwd miss does not mask a valid state-root file.
233
+ const allowed = candidates.find(p => inScope(p) && existsSync(p)) ?? candidates.find(inScope)
206
234
  if (!allowed) {
207
235
  return { ok: false, error: `路径越界:${raw}`, hint: `只读 ${root} 与 dsh 工作目录内的文本产物` }
208
236
  }
209
- const ext = allowed.slice(allowed.lastIndexOf('.') + 1).toLowerCase()
237
+ const canonical = await realpath(allowed).catch(() => null)
238
+ if (!canonical) {
239
+ return { ok: false, error: `文件不存在:${raw}`, hint: '先 gotry_artifacts_list 看在册产物' }
240
+ }
241
+ if (!(underRoot(canonical, canonicalCwd) || underRoot(canonical, canonicalRoot)) || hasDeniedSegment(canonical)) {
242
+ return { ok: false, error: `路径越界:${raw}`, hint: `只读 ${root} 与 dsh 工作目录内的文本产物` }
243
+ }
244
+ const ext = canonical.slice(canonical.lastIndexOf('.') + 1).toLowerCase()
210
245
  if (!TEXT_EXT_LANG[ext]) {
211
246
  return { ok: false, error: `不支持的文件类型 .${ext}`, hint: `白名单:${Object.keys(TEXT_EXT_LANG).join('/')}` }
212
247
  }
213
- const st = await stat(allowed).catch(() => null)
248
+ const st = await stat(canonical).catch(() => null)
214
249
  if (!st?.isFile()) return { ok: false, error: `文件不存在:${raw}`, hint: '先 gotry_artifacts_list 看在册产物' }
215
250
  if (st.size > MAX_BYTES) return { ok: false, error: `文件过大(${st.size} bytes > ${MAX_BYTES})` }
216
- filePath = allowed
217
- text = await readFile(allowed, 'utf-8')
251
+ filePath = canonical
252
+ text = await readFile(canonical, 'utf-8')
218
253
  }
219
254
 
220
255
  const allLines = text.split('\n')
@@ -224,11 +259,13 @@ export async function readArtifact(opts: {
224
259
  const ext = filePath.slice(filePath.lastIndexOf('.') + 1).toLowerCase()
225
260
  return {
226
261
  ok: true,
262
+ source: underRoot(filePath, canonicalRoot) ? 'state-root' : 'cwd',
227
263
  path: filePath,
228
264
  offset,
229
265
  lines: slice.map((t, i) => ({ number: offset + i, text: t })),
230
266
  totalLines: allLines.length,
231
267
  lang: TEXT_EXT_LANG[ext] ?? 'text',
268
+ version: createHash('sha256').update(text).digest('hex').slice(0, 12),
232
269
  content: slice.join('\n'),
233
270
  windowed: allLines.length > offset - 1 + slice.length,
234
271
  }
@@ -113,6 +113,7 @@ interface RawItem {
113
113
  seatClassName?: string
114
114
  }>
115
115
  }>
116
+ adultPrice?: string
116
117
  ticketPrice?: string
117
118
  /** 机/火与酒店条目的顶层 price(未鉴权态为打码串,机/火如 "1xxx",酒店如 "¥7xx") */
118
119
  price?: string
@@ -127,6 +128,51 @@ interface RawItem {
127
128
  detailUrl?: string
128
129
  }
129
130
 
131
+ interface ParsedItems<T> {
132
+ options: T[]
133
+ malformedCount: number
134
+ }
135
+
136
+ function nonEmptyText(value: unknown): string | undefined {
137
+ if (typeof value !== 'string') return undefined
138
+ const text = value.trim()
139
+ return text || undefined
140
+ }
141
+
142
+ function optionalText(value: unknown, allowNull = false): string | undefined | null {
143
+ if (value === undefined) return undefined
144
+ if (allowNull && value === null) return null
145
+ return nonEmptyText(value)
146
+ }
147
+
148
+ function positiveFiniteNumber(value: unknown): number | undefined {
149
+ const number = typeof value === 'number'
150
+ ? value
151
+ : typeof value === 'string' && value.trim() ? Number(value.trim()) : Number.NaN
152
+ return Number.isFinite(number) && number > 0 ? number : undefined
153
+ }
154
+
155
+ function positiveFiniteDuration(value: unknown): number | undefined {
156
+ if (typeof value === 'number') return positiveFiniteNumber(value)
157
+ const text = nonEmptyText(value)
158
+ if (!text) return undefined
159
+ const minutes = /^(\d+(?:\.\d+)?)\s*分钟$/.exec(text)?.[1]
160
+ return positiveFiniteNumber(minutes ?? text)
161
+ }
162
+
163
+ function parseTransportPrice(value: unknown): { price: number; priceRaw?: string } | undefined {
164
+ const raw = nonEmptyText(value)
165
+ if (!raw) return undefined
166
+ const bare = raw.replace(/^¥\s*/, '')
167
+ if (/^\d+(?:\.\d+)?$/.test(bare)) {
168
+ const price = Number(bare)
169
+ return Number.isFinite(price) && price > 0 ? { price } : undefined
170
+ }
171
+ // 未鉴权态可能返回 1xxx/¥7xx;保留原值,但绝不把模糊串猜成数字。
172
+ if (/^\d+x+$/.test(bare)) return { price: 0, priceRaw: raw }
173
+ return undefined
174
+ }
175
+
130
176
  /**
131
177
  * 从 CLI stdout 中提取首个完整且含 itemList 的 JSON 对象。
132
178
  *
@@ -265,7 +311,7 @@ export async function flyaiSearch(q: FlyaiQuery): Promise<FlyaiResult> {
265
311
  }
266
312
  return { ...base, latencyMs, ok: false, via: 'flyai-error', verdict: 'error', evidence: `[实时API:flyai@error@${ts}] ${r.error ?? `exit ${r.code}`}`, error: r.error ?? r.stderr.replace(/\s+/g, ' ').slice(0, 200) }
267
313
  }
268
- let items: RawItem[]
314
+ let items: unknown[]
269
315
  try {
270
316
  // main-lane 加固的扫描器:容忍 npx 前缀日志/不完整对象——data:null 语义失败
271
317
  // (issue #24)的「出发日期非法」原话经 raw stdout 片段进 error 终态,语义不丢
@@ -278,60 +324,133 @@ export async function flyaiSearch(q: FlyaiQuery): Promise<FlyaiResult> {
278
324
  }
279
325
 
280
326
  if (q.kind === 'hotel') {
281
- const hotels = parseHotelItems(items)
327
+ const parsed = parseHotelItems(items)
328
+ if (parsed.malformedCount > 0) return malformedItemListResult(q.kind, latencyMs, ts, parsed.malformedCount, items.length)
329
+ const hotels = parsed.options
282
330
  const verdict: FlyaiResult['verdict'] = hotels.length > 0 ? 'hit' : 'miss'
283
331
  return { kind: q.kind, latencyMs, ok: true, via: 'flyai', verdict, evidence: `[实时API:flyai@${ts}] ${hotels.length}/${items.length} hotel options`, hotels }
284
332
  }
285
- const options = parseTransportItems(items)
333
+ const parsed = parseTransportItems(items)
334
+ if (parsed.malformedCount > 0) return malformedItemListResult(q.kind, latencyMs, ts, parsed.malformedCount, items.length)
335
+ const options = parsed.options
286
336
  const verdict: FlyaiResult['verdict'] = options.length > 0 ? 'hit' : 'miss'
287
337
  return { kind: q.kind, latencyMs, ok: true, via: 'flyai', verdict, evidence: `[实时API:flyai@${ts}] ${options.length}/${items.length} ${q.kind} options`, options }
288
338
  }
289
339
 
290
- /** 机/火条目 FlyaiOption(journeys[0].segments[0];缺航班号/时刻的条目跳过) */
291
- function parseTransportItems(items: RawItem[]): FlyaiOption[] {
340
+ function malformedItemListResult(kind: FlyaiKind, latencyMs: number, ts: string, malformedCount: number, total: number): FlyaiResult {
341
+ const label = kind === 'hotel' ? 'hotel' : 'transport'
342
+ const error = `FlyAI ${label} itemList malformed: ${malformedCount}/${total} item(s) failed typed validation`
343
+ return {
344
+ kind,
345
+ latencyMs,
346
+ ok: false,
347
+ via: 'flyai-error',
348
+ verdict: 'error',
349
+ evidence: `[实时API:flyai@error@${ts}] ${label} itemList malformed (${malformedCount}/${total} items failed typed validation; valid siblings discarded)`,
350
+ error,
351
+ }
352
+ }
353
+
354
+ /** 机/火条目 → FlyaiOption;非空列表含任一不完整条目即整体 malformed */
355
+ function parseTransportItems(items: unknown[]): ParsedItems<FlyaiOption> {
292
356
  const options: FlyaiOption[] = []
293
- for (const it of items) {
294
- const seg = it.journeys?.[0]?.segments?.[0]
295
- if (!seg?.marketingTransportNo || !seg.depDateTime) continue
296
- const rawPrice = it.ticketPrice ?? it.price ?? ''
297
- const numericPrice = Number(rawPrice)
357
+ let malformedCount = 0
358
+ for (const raw of items) {
359
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
360
+ malformedCount += 1
361
+ continue
362
+ }
363
+ const it = raw as RawItem
364
+ const journey = Array.isArray(it.journeys) ? it.journeys[0] : undefined
365
+ const seg = journey && typeof journey === 'object' && !Array.isArray(journey) && Array.isArray(journey.segments)
366
+ ? journey.segments[0]
367
+ : undefined
368
+ if (!seg || typeof seg !== 'object' || Array.isArray(seg)) {
369
+ malformedCount += 1
370
+ continue
371
+ }
372
+ const no = nonEmptyText(seg.marketingTransportNo)
373
+ const name = optionalText(seg.marketingTransportName)
374
+ const depDateTime = nonEmptyText(seg.depDateTime)
375
+ const arrDateTime = nonEmptyText(seg.arrDateTime)
376
+ const depStation = nonEmptyText(seg.depStationName)
377
+ const arrStation = nonEmptyText(seg.arrStationName)
378
+ const durationMin = positiveFiniteDuration(seg.duration)
379
+ const rawPriceValue = it.adultPrice !== undefined
380
+ ? it.adultPrice
381
+ : it.ticketPrice !== undefined
382
+ ? it.ticketPrice
383
+ : it.price
384
+ const parsedPrice = parseTransportPrice(rawPriceValue)
385
+ const seatClass = optionalText(seg.seatClassName)
386
+ const jumpUrl = optionalText(it.jumpUrl)
387
+ if (!no || !depDateTime || !arrDateTime || !depStation || !arrStation || durationMin === undefined || !parsedPrice
388
+ || (seg.marketingTransportName !== undefined && name === undefined)
389
+ || (seg.seatClassName !== undefined && seatClass === undefined)
390
+ || (it.jumpUrl !== undefined && jumpUrl === undefined)) {
391
+ malformedCount += 1
392
+ continue
393
+ }
298
394
  options.push({
299
- no: seg.marketingTransportNo,
300
- name: seg.marketingTransportName ?? '',
301
- depDateTime: seg.depDateTime ?? '',
302
- arrDateTime: seg.arrDateTime ?? '',
303
- depStation: seg.depStationName ?? '',
304
- arrStation: seg.arrStationName ?? '',
305
- durationMin: Number(seg.duration ?? 0) || 0,
306
- price: Number.isFinite(numericPrice) && numericPrice > 0 ? numericPrice : 0,
307
- priceRaw: /^\d+$/.test(rawPrice) ? undefined : rawPrice || undefined,
308
- seatClass: seg.seatClassName,
309
- jumpUrl: it.jumpUrl,
395
+ no,
396
+ name: name ?? '',
397
+ depDateTime,
398
+ arrDateTime,
399
+ depStation,
400
+ arrStation,
401
+ durationMin,
402
+ price: parsedPrice.price,
403
+ priceRaw: parsedPrice.priceRaw,
404
+ seatClass: seatClass ?? undefined,
405
+ jumpUrl: jumpUrl ?? undefined,
310
406
  })
311
407
  }
312
- return options
408
+ return { options, malformedCount }
313
409
  }
314
410
 
315
- /** 酒店条目 → FlyaiHotelOption(实测 2026-08-29:price 未鉴权为打码串"¥7xx",rate 常 null) */
316
- function parseHotelItems(items: RawItem[]): FlyaiHotelOption[] {
411
+ /** 酒店条目 → FlyaiHotelOption;非空列表含任一不完整条目即整体 malformed */
412
+ function parseHotelItems(items: unknown[]): ParsedItems<FlyaiHotelOption> {
317
413
  const hotels: FlyaiHotelOption[] = []
318
- for (const it of items) {
319
- if (!it.name) continue
320
- const rawPrice = String(it.price ?? '')
414
+ let malformedCount = 0
415
+ for (const raw of items) {
416
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
417
+ malformedCount += 1
418
+ continue
419
+ }
420
+ const it = raw as RawItem
421
+ const name = nonEmptyText(it.name)
422
+ const star = optionalText(it.star)
423
+ const rawPrice = optionalText(it.price)
424
+ const rate = optionalText(it.rate, true)
425
+ const address = optionalText(it.address)
426
+ const poi = optionalText(it.interestsPoi)
427
+ const hotelId = optionalText(it.shId)
428
+ const jumpUrl = optionalText(it.detailUrl)
429
+ if (!name
430
+ || (it.star !== undefined && star === undefined)
431
+ || (it.price !== undefined && rawPrice === undefined)
432
+ || (it.rate !== undefined && rate === undefined)
433
+ || (it.address !== undefined && address === undefined)
434
+ || (it.interestsPoi !== undefined && poi === undefined)
435
+ || (it.shId !== undefined && hotelId === undefined)
436
+ || (it.detailUrl !== undefined && jumpUrl === undefined)) {
437
+ malformedCount += 1
438
+ continue
439
+ }
321
440
  // 打码价如 "¥7xx" 绝不能截成数字 7(会伪装成真价)——仅全数字串才落 price
322
- const bare = rawPrice.replace(/^[¥]/, '')
441
+ const bare = (rawPrice ?? '').replace(/^[¥]/, '')
323
442
  const numericPrice = Number(bare)
324
443
  hotels.push({
325
- name: it.name,
326
- star: it.star,
444
+ name,
445
+ star: star ?? undefined,
327
446
  price: /^\d+(\.\d+)?$/.test(bare) && numericPrice > 0 ? numericPrice : 0,
328
447
  priceRaw: rawPrice || undefined,
329
- rate: it.rate != null ? String(it.rate) : undefined,
330
- address: it.address,
331
- poi: it.interestsPoi,
332
- hotelId: it.shId,
333
- jumpUrl: it.detailUrl,
448
+ rate: rate ?? undefined,
449
+ address: address ?? undefined,
450
+ poi: poi ?? undefined,
451
+ hotelId: hotelId ?? undefined,
452
+ jumpUrl: jumpUrl ?? undefined,
334
453
  })
335
454
  }
336
- return hotels
337
- }
455
+ return { options: hotels, malformedCount }
456
+ }
@@ -207,7 +207,7 @@ export async function searchHotels(
207
207
  // 自动跑过官方脚本;PATH 未含 ~/.local/bin 时上方候选路径也已兜住),
208
208
  // 裸 "spawn hbcli ENOENT" 读起来像工具坏了——实际静态包降级是设计行为
209
209
  const rawReason = live.error ?? live.via
210
- const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx gotry setup 可按官方脚本安装)' : rawReason
210
+ const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx @danceiny/gotry setup 可按官方脚本安装)' : rawReason
211
211
  // 降级:读静态包,按目的地过滤命中的住宿块(issue #24)——整包倾倒会把无关场景
212
212
  // (深圳/普吉/曼谷/云南/大理混装)灌给模型且不指明哪块相关;包内无该目的地时明示
213
213
  // 「无数据」而不是伪装成可用结果。
@@ -1,20 +1,17 @@
1
1
  /**
2
- * 进程级事故日志(D-NEW 护栏):fsync append-only JSONL
2
+ * 进程级事故日志(D-NEW):同步、durable JSONL 诊断记录。
3
3
  *
4
- * 目的: dsh 0.1.1-rc.1 只接 SIGINT/SIGTERM,缺 uncaughtException/unhandledRejection——
5
- * 任一插件 wasm/runtime 异常穿透到 node 层都会让整个 web UI 进程死亡,
6
- * 现场不存。这次(Z3 WASM mk_bool_var 崩溃)教训:连个取证都没有。
7
- *
8
- * 设计: 进程级 handler 同步写盘(fsync),尽量在进程被杀前留下事故证据。
9
- * handler 自身不再抛——再次抛出会绕过 handler,变成无声死亡。
4
+ * `uncaughtExceptionMonitor` 只观察 fatal 事故并留下证据;宿主仍决定
5
+ * 是否退出。GoTry 不吞掉 fatal 事件、不调用 process.exit、也不隐式重启。
6
+ * 工具 execute 的业务失败仍由 guardToolExecute 转成结构化失败返回。
10
7
  *
11
8
  * §11 状态面同步:事故日志是 gotry-state/incidents.jsonl,
12
- * 用户可见(README 第 x 节),可清理。
9
+ * 用户可见、可清理。
13
10
  */
14
11
 
15
- import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
12
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, writeFileSync } from 'node:fs'
16
13
  import { dirname, isAbsolute, join } from 'node:path'
17
- import { fileURLToPath } from 'node:url'
14
+ import { pathToFileURL } from 'node:url'
18
15
  import type { ToolFailure } from '../src/tool-packet.ts'
19
16
 
20
17
  export type IncidentKind = 'uncaughtException' | 'unhandledRejection' | 'plugin_error' | 'tool_execute_error'
@@ -43,68 +40,104 @@ function ensureDir(path: string): void {
43
40
 
44
41
  let installed = false
45
42
  let logPath: string | null = null
43
+ let activeMonitor: ((err: Error, origin: NodeJS.UncaughtExceptionOrigin) => void) | null = null
44
+
45
+ function safeErrorMessage(value: unknown): string {
46
+ try {
47
+ if (value instanceof Error) return typeof value.message === 'string' ? value.message : '[error-without-message]'
48
+ if (typeof value === 'string') return value
49
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' || typeof value === 'symbol') return String(value)
50
+ const json = JSON.stringify(value)
51
+ return typeof json === 'string' ? json : '[unprintable-error]'
52
+ } catch {
53
+ return '[unprintable-error]'
54
+ }
55
+ }
56
+
57
+ function safeErrorStack(value: unknown): string {
58
+ try {
59
+ const stack = (value as { stack?: unknown } | null | undefined)?.stack
60
+ return typeof stack === 'string' ? stack : ''
61
+ } catch {
62
+ return ''
63
+ }
64
+ }
46
65
 
47
66
  /**
48
- * 写入一条事故并 fsync。同步执行——handler 内异步无效。
49
- * 自身 try/catch 自保,不让写入失败再抛一次把进程杀掉。
67
+ * 写入一条事故并 fsync。整个 append 使用同一个 fd,并保证所有失败路径
68
+ * 都尝试 close;返回 true 只表示写入、fsync、close 全部成功。
50
69
  */
51
70
  export function recordIncident(inc: Incident, stateRoot?: string): boolean {
52
71
  const target = stateRoot ? resolveIncidentsPath(stateRoot) : (logPath ?? resolveIncidentsPath('.'))
72
+ let fd: number | undefined
73
+ let closeAttempted = false
53
74
  try {
54
75
  ensureDir(target)
55
- appendFileSync(target, JSON.stringify(inc) + '\n', { flag: 'a' })
56
- // fsync: 让盘数据落定,免得被杀时缓冲丢
57
- const fd = require('node:fs').openSync(target, 'a')
58
- require('node:fs').fsyncSync(fd)
59
- require('node:fs').closeSync(fd)
76
+ fd = openSync(target, 'a')
77
+ writeFileSync(fd, JSON.stringify(inc) + '\n', { encoding: 'utf8' })
78
+ fsyncSync(fd)
79
+ closeAttempted = true
80
+ closeSync(fd)
81
+ fd = undefined
60
82
  return true
61
83
  } catch {
62
- // 绝对不能在这里抛——我们正在处理已经抛过一次的进程级错误
84
+ // 事故处理中的诊断失败绝不能再次抛出。
63
85
  return false
86
+ } finally {
87
+ if (fd !== undefined && !closeAttempted) {
88
+ closeAttempted = true
89
+ try { closeSync(fd) } catch { /* best effort close on a failed write */ }
90
+ }
64
91
  }
65
92
  }
66
93
 
67
94
  /**
68
- * 安装进程级护栏:进程整个生命周期仅生效一次(重复调用安全)。
69
- * 必须同步执行——异步发生在事件循环下一次,而 wasm 崩溃后已无下一轮。
70
- *
71
- * @returns 一个 disposer,卸载所有监听(测试场景使用)
95
+ * 安装进程级事故观察器。只挂 monitor,不改变 Node/dsh 的 fatal 行为。
96
+ * 进程整个生命周期仅生效一次;disposer 供宿主卸载和测试使用。
72
97
  */
73
98
  export function installProcessGuards(stateRoot: string, labels?: { uncaughtException?: string; unhandledRejection?: string }): () => void {
74
99
  if (installed) return () => { /* 已装,no-op */ }
75
100
  installed = true
76
101
  logPath = resolveIncidentsPath(stateRoot)
77
102
 
78
- const uncaughtHandler = (err: Error, origin: NodeJS.UncaughtExceptionOrigin | 'uncaughtException') => {
79
- recordIncident({
80
- ts: new Date().toISOString(),
81
- kind: 'uncaughtException',
82
- message: (err?.message ?? String(err)).slice(0, 2000),
83
- stack: (err?.stack ?? '').slice(0, 4000),
84
- source: labels?.uncaughtException ?? String(origin),
85
- }, stateRoot)
86
- // 不调用 process.exit——让 dsh/上级容器的重启策略负责;
87
- // 如果 dsh 不接住,我们至少留下事故记录。
88
- }
89
-
90
- const rejectionHandler = (reason: unknown, promise: Promise<unknown>) => {
91
- const msg = reason instanceof Error ? reason.message : String(reason)
92
- const stack = reason instanceof Error ? (reason.stack ?? '') : ''
93
- recordIncident({
94
- ts: new Date().toISOString(),
95
- kind: 'unhandledRejection',
96
- message: msg.slice(0, 2000),
97
- stack: stack.slice(0, 4000),
98
- source: labels?.unhandledRejection ?? 'promise',
99
- }, stateRoot)
103
+ const monitor = (err: Error, origin: NodeJS.UncaughtExceptionOrigin) => {
104
+ try {
105
+ const kind: IncidentKind = origin === 'unhandledRejection' ? 'unhandledRejection' : 'uncaughtException'
106
+ const source = kind === 'unhandledRejection'
107
+ ? (labels?.unhandledRejection ?? 'promise')
108
+ : (labels?.uncaughtException ?? origin)
109
+ recordIncident({
110
+ ts: new Date().toISOString(),
111
+ kind,
112
+ message: safeErrorMessage(err).slice(0, 2000),
113
+ stack: safeErrorStack(err).slice(0, 4000),
114
+ source,
115
+ }, stateRoot)
116
+ } catch {
117
+ // Formatting must never replace the original fatal event. Keep a final
118
+ // plain fallback attempt for diagnostics, while the host retains control.
119
+ try {
120
+ recordIncident({
121
+ ts: new Date().toISOString(),
122
+ kind: origin === 'unhandledRejection' ? 'unhandledRejection' : 'uncaughtException',
123
+ message: '[incident-observer-fallback]',
124
+ source: 'incident-observer',
125
+ }, stateRoot)
126
+ } catch { /* observer is strictly best effort */ }
127
+ }
100
128
  }
101
129
 
102
- process.on('uncaughtException', uncaughtHandler)
103
- process.on('unhandledRejection', rejectionHandler)
130
+ process.on('uncaughtExceptionMonitor', monitor)
131
+ activeMonitor = monitor
132
+ let active = true
104
133
  return () => {
105
- process.off('uncaughtException', uncaughtHandler)
106
- process.off('unhandledRejection', rejectionHandler)
134
+ if (!active) return
135
+ active = false
136
+ if (activeMonitor !== monitor) return
137
+ process.off('uncaughtExceptionMonitor', monitor)
107
138
  installed = false
139
+ logPath = null
140
+ activeMonitor = null
108
141
  }
109
142
  }
110
143
 
@@ -112,24 +145,23 @@ export function installProcessGuards(stateRoot: string, labels?: { uncaughtExcep
112
145
  * 工具执行面异常隔离(D-NEW gotry 侧收尾):dsh 的一个工具 execute 抛错/拒绝
113
146
  * 会沿 cordis 传到主循环,拖垮整个会话。包装后:降级为结构化错误返回给 LLM、
114
147
  * 事故落盘,永不向上抛。落盘失败也不抛(双保险,仍返回结构化错误)。
115
- * 失败形状 = ToolFailure(RFC S1 observation envelope 的失败分支,编译期对齐)。
116
148
  */
117
149
  export function guardToolExecute<A, R>(name: string, stateRoot: string, execute: (args: A, exec: unknown) => R | Promise<R>): (args: A, exec: unknown) => Promise<R> {
118
150
  return async (args: A, exec: unknown): Promise<R> => {
119
151
  try {
120
152
  return await execute(args, exec)
121
153
  } catch (e) {
122
- const err = e instanceof Error ? e : new Error(String(e))
154
+ const message = safeErrorMessage(e)
123
155
  recordIncident({
124
156
  ts: new Date().toISOString(),
125
157
  kind: 'tool_execute_error',
126
- message: `${name}: ${err.message}`.slice(0, 2000),
127
- stack: (err.stack ?? '').slice(0, 4000),
158
+ message: `${name}: ${message}`.slice(0, 2000),
159
+ stack: safeErrorStack(e).slice(0, 4000),
128
160
  source: name,
129
161
  }, stateRoot)
130
162
  const failure: ToolFailure = {
131
163
  ok: false,
132
- summary: `gotry_${name} 内部错误(已隔离,会话继续): ${err.message.slice(0, 300)}`,
164
+ summary: `gotry_${name} 内部错误(已隔离,会话继续): ${message.slice(0, 300)}`,
133
165
  evidence: `[incident:tool_execute_error@${new Date().toISOString()}]`,
134
166
  }
135
167
  return failure as R
@@ -137,12 +169,9 @@ export function guardToolExecute<A, R>(name: string, stateRoot: string, execute:
137
169
  }
138
170
  }
139
171
 
140
- /** CLI 调试:从命令行单独跑查看当前进程是否已挂护栏 */
141
- if (import.meta.url === `file://${fileURLToPath(import.meta.url)}` && process.argv[2] === '--smoke') {
172
+ /** CLI 调试:最小 fatal 子进程会留下事故证据并按 Node 原生语义非零退出。 */
173
+ if (process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url && process.argv[2] === '--smoke') {
142
174
  const stateRoot = process.argv[3] ?? '.'
143
175
  installProcessGuards(stateRoot, { uncaughtException: 'smoke', unhandledRejection: 'smoke' })
144
- writeFileSync(resolveIncidentsPath(stateRoot) + '.smoke', 'ok')
145
- // 触发一次未捕获,应被写盘(不真正杀进程——进程继续跑完)
146
- setTimeout(() => Promise.reject(new Error('intentional smoke-rejection')), 50)
147
- setTimeout(() => { console.log('SMOKE OK'); process.exit(0) }, 500)
176
+ setTimeout(() => { throw new Error('intentional smoke-uncaught') }, 50)
148
177
  }