@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.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,123 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 旅行时间线单测(memory-design P1,travel_timeline.v1):
|
|
3
|
+
* 守门(必填/绝对日期/幂等/重叠冲突即停)/投影/交叉一致(verified 无 timeline 缺口)。
|
|
4
|
+
* 运行:cd ts && npx tsx scripts/travel-timeline-tests.ts
|
|
5
|
+
*/
|
|
1
6
|
import assert from 'node:assert/strict';
|
|
2
7
|
import { appendTrip, makeTripId, projectTimeline, timelineGapsForVerified, resolveTimelineDate } from '../src/travel-timeline.js';
|
|
3
8
|
import { buildTimeAnchor } from '../src/time-anchor.js';
|
|
4
|
-
const anchor = buildTimeAnchor(new Date(2026, 7, 28, 12));
|
|
9
|
+
const anchor = buildTimeAnchor(new Date(2026, 7, 28, 12)); // 2026-08-28
|
|
5
10
|
let n = 0;
|
|
6
11
|
function pass(name, body) {
|
|
7
12
|
body();
|
|
8
13
|
console.log(` ${++n}. ${name} OK`);
|
|
9
14
|
}
|
|
10
|
-
pass('守门:必填与绝对日期(不猜)', ()=>{
|
|
11
|
-
const ev = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
17
|
-
assert.equal(appendTrip([], {
|
|
18
|
-
...ev,
|
|
19
|
-
destination: ''
|
|
20
|
-
}).appended, false, '无目的地拒');
|
|
21
|
-
assert.equal(appendTrip([], {
|
|
22
|
-
...ev,
|
|
23
|
-
evidence: ''
|
|
24
|
-
}).appended, false, '无 evidence 拒(溯源 P0)');
|
|
25
|
-
assert.ok(appendTrip([], {
|
|
26
|
-
...ev,
|
|
27
|
-
start: '下周一'
|
|
28
|
-
}).reason?.includes('YYYY-MM-DD'), '词表外日期拒收(不猜)');
|
|
29
|
-
assert.ok(appendTrip([], {
|
|
30
|
-
...ev,
|
|
31
|
-
end: '2025-09-30'
|
|
32
|
-
}).reason?.includes('end 早于 start'), 'end<start 拒');
|
|
15
|
+
pass('守门:必填与绝对日期(不猜)', () => {
|
|
16
|
+
const ev = { destination: '大理', start: '2025-10-01', source: 'user-verbatim', evidence: '用户原话' };
|
|
17
|
+
assert.equal(appendTrip([], { ...ev, destination: '' }).appended, false, '无目的地拒');
|
|
18
|
+
assert.equal(appendTrip([], { ...ev, evidence: '' }).appended, false, '无 evidence 拒(溯源 P0)');
|
|
19
|
+
assert.ok(appendTrip([], { ...ev, start: '下周一' }).reason?.includes('YYYY-MM-DD'), '词表外日期拒收(不猜)');
|
|
20
|
+
assert.ok(appendTrip([], { ...ev, end: '2025-09-30' }).reason?.includes('end 早于 start'), 'end<start 拒');
|
|
33
21
|
});
|
|
34
|
-
pass('幂等:同目的地+start+source = 同一行程 no-op', ()=>{
|
|
35
|
-
const ev = {
|
|
36
|
-
destination: '大理',
|
|
37
|
-
start: '2025-10-01',
|
|
38
|
-
source: 'user-verbatim',
|
|
39
|
-
evidence: '原话'
|
|
40
|
-
};
|
|
22
|
+
pass('幂等:同目的地+start+source = 同一行程 no-op', () => {
|
|
23
|
+
const ev = { destination: '大理', start: '2025-10-01', source: 'user-verbatim', evidence: '原话' };
|
|
41
24
|
const r1 = appendTrip([], ev);
|
|
42
25
|
assert.equal(r1.appended, true);
|
|
43
|
-
const r2 = appendTrip(r1.events, {
|
|
44
|
-
...ev,
|
|
45
|
-
evidence: '换个说法再说一遍'
|
|
46
|
-
});
|
|
26
|
+
const r2 = appendTrip(r1.events, { ...ev, evidence: '换个说法再说一遍' });
|
|
47
27
|
assert.equal(r2.appended, false, '同 trip_id 重放 no-op');
|
|
48
28
|
assert.equal(r2.tripId, r1.tripId);
|
|
49
29
|
});
|
|
50
|
-
pass('重叠冲突即停:同目的地日期区间重叠拒收', ()=>{
|
|
51
|
-
const r1 = appendTrip([], {
|
|
52
|
-
|
|
53
|
-
start: '2025-10-01',
|
|
54
|
-
end: '2025-10-05',
|
|
55
|
-
source: 'user-verbatim',
|
|
56
|
-
evidence: '原话'
|
|
57
|
-
});
|
|
58
|
-
const r2 = appendTrip(r1.events, {
|
|
59
|
-
destination: '大理',
|
|
60
|
-
start: '2025-10-04',
|
|
61
|
-
source: 'user-verbatim',
|
|
62
|
-
evidence: '原话2'
|
|
63
|
-
});
|
|
30
|
+
pass('重叠冲突即停:同目的地日期区间重叠拒收', () => {
|
|
31
|
+
const r1 = appendTrip([], { destination: '大理', start: '2025-10-01', end: '2025-10-05', source: 'user-verbatim', evidence: '原话' });
|
|
32
|
+
const r2 = appendTrip(r1.events, { destination: '大理', start: '2025-10-04', source: 'user-verbatim', evidence: '原话2' });
|
|
64
33
|
assert.equal(r2.appended, false);
|
|
65
34
|
assert.ok(r2.reason?.includes('重叠'), '冲突理由说明');
|
|
66
35
|
});
|
|
67
|
-
pass('日期解析:锚点卡词表进、词表外 null(经 resolveTimelineDate)', ()=>{
|
|
36
|
+
pass('日期解析:锚点卡词表进、词表外 null(经 resolveTimelineDate)', () => {
|
|
68
37
|
assert.equal(resolveTimelineDate('2025-10-01', anchor), '2025-10-01');
|
|
69
38
|
assert.equal(resolveTimelineDate('去年国庆', anchor), null, '开放表达不猜');
|
|
70
39
|
});
|
|
71
|
-
pass('投影:start 倒序(最近在前)', ()=>{
|
|
40
|
+
pass('投影:start 倒序(最近在前)', () => {
|
|
72
41
|
let evs = [];
|
|
73
|
-
evs = appendTrip(evs, {
|
|
74
|
-
|
|
75
|
-
start: '2026-07-10',
|
|
76
|
-
source: 'user-verbatim',
|
|
77
|
-
evidence: 'a'
|
|
78
|
-
}).events;
|
|
79
|
-
evs = appendTrip(evs, {
|
|
80
|
-
destination: '大理',
|
|
81
|
-
start: '2025-10-01',
|
|
82
|
-
source: 'user-verbatim',
|
|
83
|
-
evidence: 'b'
|
|
84
|
-
}).events;
|
|
42
|
+
evs = appendTrip(evs, { destination: '普吉', start: '2026-07-10', source: 'user-verbatim', evidence: 'a' }).events;
|
|
43
|
+
evs = appendTrip(evs, { destination: '大理', start: '2025-10-01', source: 'user-verbatim', evidence: 'b' }).events;
|
|
85
44
|
const p = projectTimeline(evs);
|
|
86
45
|
assert.equal(p[0]?.destination, '普吉');
|
|
87
46
|
assert.equal(p.length, 2);
|
|
88
47
|
});
|
|
89
|
-
pass('交叉一致:verified wish 无 timeline = 缺口暴露(巡检口径,不自动补写)', ()=>{
|
|
90
|
-
const evs = appendTrip([], {
|
|
91
|
-
destination: '大理',
|
|
92
|
-
start: '2025-10-01',
|
|
93
|
-
source: 'wish-confirmed',
|
|
94
|
-
evidence: 'wish w1 confirm'
|
|
95
|
-
}).events;
|
|
48
|
+
pass('交叉一致:verified wish 无 timeline = 缺口暴露(巡检口径,不自动补写)', () => {
|
|
49
|
+
const evs = appendTrip([], { destination: '大理', start: '2025-10-01', source: 'wish-confirmed', evidence: 'wish w1 confirm' }).events;
|
|
96
50
|
const gaps = timelineGapsForVerified(evs, [
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
destination: '大理'
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
wishId: 'w2',
|
|
103
|
-
destination: '清迈'
|
|
104
|
-
}
|
|
105
|
-
]);
|
|
106
|
-
assert.deepEqual(gaps, [
|
|
107
|
-
{
|
|
108
|
-
wishId: 'w2'
|
|
109
|
-
}
|
|
51
|
+
{ wishId: 'w1', destination: '大理' },
|
|
52
|
+
{ wishId: 'w2', destination: '清迈' },
|
|
110
53
|
]);
|
|
54
|
+
assert.deepEqual(gaps, [{ wishId: 'w2' }]);
|
|
111
55
|
});
|
|
112
|
-
pass('trip_id 语义派生:确定性可重放', ()=>{
|
|
113
|
-
assert.equal(makeTripId({
|
|
114
|
-
destination: '大 理',
|
|
115
|
-
start: '2025-10-01',
|
|
116
|
-
source: 'user-verbatim',
|
|
117
|
-
evidence: 'x'
|
|
118
|
-
}), '大_理|2025-10-01|user-verbatim');
|
|
56
|
+
pass('trip_id 语义派生:确定性可重放', () => {
|
|
57
|
+
assert.equal(makeTripId({ destination: '大 理', start: '2025-10-01', source: 'user-verbatim', evidence: 'x' }), '大_理|2025-10-01|user-verbatim');
|
|
119
58
|
});
|
|
120
59
|
console.log(`\nTRAVEL TIMELINE TESTS: ${n}/7 OK(memory-design P1 守门面)`);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//# sourceURL=ts/scripts/travel-timeline-tests.ts
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-handoff 收集器闭环测试(ADR-24 v2 收集半段)。
|
|
3
|
+
* 假 planner(node 脚本)驱动:成功/失败/空输出/幂等复诵零重算/--all/
|
|
4
|
+
* 缺单;并断言递归防护真的传进了子进程(GOTRY_HANDOFF_CHILD=1 且父进程
|
|
5
|
+
* 的数值 pin/工单根被清除)。全程隔离 stateRoot,不触真实 gotry-state。
|
|
6
|
+
*/
|
|
1
7
|
import assert from 'node:assert/strict';
|
|
2
8
|
import { execFileSync } from 'node:child_process';
|
|
3
9
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -7,41 +13,25 @@ import { listTurnHandoffTickets, settleTurnHandoffTicket, writeTurnHandoffTicket
|
|
|
7
13
|
const TS_DIR = join(import.meta.dirname, '..');
|
|
8
14
|
const TSX = join(TS_DIR, 'node_modules', 'tsx', 'dist', 'cli.mjs');
|
|
9
15
|
function runCollector(args, env = {}) {
|
|
16
|
+
// 用 execFileSync 的同步抓取替代 spawn 手工聚合,行为等价且断言更直接。
|
|
10
17
|
try {
|
|
11
|
-
const stdout = execFileSync(process.execPath, [
|
|
12
|
-
TSX,
|
|
13
|
-
'scripts/turn-handoff-collect.ts',
|
|
14
|
-
...args
|
|
15
|
-
], {
|
|
18
|
+
const stdout = execFileSync(process.execPath, [TSX, 'scripts/turn-handoff-collect.ts', ...args], {
|
|
16
19
|
cwd: TS_DIR,
|
|
17
|
-
env: {
|
|
18
|
-
...process.env,
|
|
19
|
-
...env
|
|
20
|
-
},
|
|
20
|
+
env: { ...process.env, ...env },
|
|
21
21
|
encoding: 'utf-8',
|
|
22
|
-
stdio: [
|
|
23
|
-
'ignore',
|
|
24
|
-
'pipe',
|
|
25
|
-
'pipe'
|
|
26
|
-
]
|
|
22
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
27
23
|
});
|
|
28
|
-
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
stderr: ''
|
|
32
|
-
};
|
|
33
|
-
} catch (error) {
|
|
24
|
+
return { status: 0, stdout, stderr: '' };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
34
27
|
const e = error;
|
|
35
|
-
return {
|
|
36
|
-
status: e.status ?? null,
|
|
37
|
-
stdout: e.stdout ?? '',
|
|
38
|
-
stderr: e.stderr ?? ''
|
|
39
|
-
};
|
|
28
|
+
return { status: e.status ?? null, stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
|
|
40
29
|
}
|
|
41
30
|
}
|
|
42
31
|
const root = mkdtempSync(join(tmpdir(), 'gotry-handoff-collect-'));
|
|
43
32
|
const planners = mkdtempSync(join(tmpdir(), 'gotry-handoff-planners-'));
|
|
44
33
|
const plannerCalls = join(planners, 'calls.log');
|
|
34
|
+
// ok:记录调用环境(递归防护证据)→ 输出交付物
|
|
45
35
|
const okPlanner = join(planners, 'ok.mjs');
|
|
46
36
|
writeFileSync(okPlanner, `import { appendFileSync } from 'node:fs'
|
|
47
37
|
appendFileSync(${JSON.stringify(plannerCalls)}, [
|
|
@@ -61,20 +51,14 @@ const emptyPlanner = join(planners, 'empty.mjs');
|
|
|
61
51
|
writeFileSync(emptyPlanner, `import { appendFileSync } from 'node:fs'
|
|
62
52
|
appendFileSync(${JSON.stringify(plannerCalls)}, 'call empty\\n')
|
|
63
53
|
`);
|
|
64
|
-
const calls = ()=>existsSync(plannerCalls) ? readFileSync(plannerCalls, 'utf-8').trim().split('\n') : [];
|
|
54
|
+
const calls = () => (existsSync(plannerCalls) ? readFileSync(plannerCalls, 'utf-8').trim().split('\n') : []);
|
|
65
55
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]).status, 1);
|
|
56
|
+
// 1) 缺单 → exit 1
|
|
57
|
+
assert.equal(runCollector(['th-missing', root]).status, 1);
|
|
58
|
+
// 2) 成功回收:planner 恰跑一次,工单 settled,交付物含方案,终态 JSON
|
|
70
59
|
const t1 = await writeTurnHandoffTicket(root, '帮我规划10.3衡阳婚礼十几天行程');
|
|
71
60
|
{
|
|
72
|
-
const r = runCollector([
|
|
73
|
-
t1.id,
|
|
74
|
-
root
|
|
75
|
-
], {
|
|
76
|
-
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
77
|
-
});
|
|
61
|
+
const r = runCollector([t1.id, root], { GOTRY_HANDOFF_PLANNER_BIN: okPlanner });
|
|
78
62
|
assert.equal(r.status, 0, `stderr=${r.stderr}`);
|
|
79
63
|
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
80
64
|
assert.equal(terminal['schema'], 'gotry_turn_handoff_terminal.v1');
|
|
@@ -86,37 +70,28 @@ try {
|
|
|
86
70
|
assert.ok(t1After['settledAt']);
|
|
87
71
|
const deliverable1 = readFileSync(join(root, 'gotry-state', 'turn-handoffs', `${t1.id}.deliverable.md`), 'utf8');
|
|
88
72
|
assert.ok(deliverable1.includes('方案A') && deliverable1.includes(t1.id));
|
|
73
|
+
// 3) 幂等复诵:重跑 exit 0,同交付物,planner 零重算
|
|
89
74
|
{
|
|
90
75
|
const before = calls().length;
|
|
91
|
-
const r = runCollector([
|
|
92
|
-
t1.id,
|
|
93
|
-
root
|
|
94
|
-
], {
|
|
95
|
-
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
96
|
-
});
|
|
76
|
+
const r = runCollector([t1.id, root], { GOTRY_HANDOFF_PLANNER_BIN: okPlanner });
|
|
97
77
|
assert.equal(r.status, 0);
|
|
98
78
|
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
99
79
|
assert.equal(terminal['status'], 'succeeded');
|
|
100
80
|
assert.equal(calls().length, before, 'settled ticket must not re-run the planner');
|
|
101
81
|
}
|
|
82
|
+
// 4) 递归防护证据:ok planner 记录的子环境
|
|
102
83
|
{
|
|
103
|
-
const last = [
|
|
104
|
-
...calls()
|
|
105
|
-
].reverse().find((l)=>l.includes('child='));
|
|
84
|
+
const last = [...calls()].reverse().find(l => l.includes('child='));
|
|
106
85
|
assert.ok(last, 'planner ran');
|
|
107
86
|
assert.ok(last.includes('child=1'), `GOTRY_HANDOFF_CHILD must be 1: ${last}`);
|
|
108
87
|
assert.ok(last.includes('pin=CLEARED'), `parent deadline pins must be cleared: ${last}`);
|
|
109
88
|
assert.ok(last.includes('root=CLEARED'), `parent handoff root must be cleared: ${last}`);
|
|
110
89
|
assert.ok(last.includes('msg=帮我规划10.3衡阳婚礼'), 'planner receives the verbatim user ask');
|
|
111
90
|
}
|
|
91
|
+
// 5) 失败回收:exit 2,工单 failed + error,交付物是诚实失败说明
|
|
112
92
|
const t2 = await writeTurnHandoffTicket(root, '另一趟失败的规划');
|
|
113
93
|
{
|
|
114
|
-
const r = runCollector([
|
|
115
|
-
t2.id,
|
|
116
|
-
root
|
|
117
|
-
], {
|
|
118
|
-
GOTRY_HANDOFF_PLANNER_BIN: failPlanner
|
|
119
|
-
});
|
|
94
|
+
const r = runCollector([t2.id, root], { GOTRY_HANDOFF_PLANNER_BIN: failPlanner });
|
|
120
95
|
assert.equal(r.status, 2);
|
|
121
96
|
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
122
97
|
assert.equal(terminal['status'], 'failed');
|
|
@@ -127,28 +102,23 @@ try {
|
|
|
127
102
|
assert.ok(String(t2After['error']).startsWith('planner exited 3'));
|
|
128
103
|
const deliverable2 = readFileSync(join(root, 'gotry-state', 'turn-handoffs', `${t2.id}.deliverable.md`), 'utf8');
|
|
129
104
|
assert.ok(deliverable2.includes('没有完成') && deliverable2.includes('planner exited 3'), 'honest failure note');
|
|
105
|
+
// 失败复诵:exit 2,零重算
|
|
130
106
|
{
|
|
131
107
|
const before = calls().length;
|
|
132
|
-
assert.equal(runCollector([
|
|
133
|
-
t2.id,
|
|
134
|
-
root
|
|
135
|
-
], {
|
|
136
|
-
GOTRY_HANDOFF_PLANNER_BIN: failPlanner
|
|
137
|
-
}).status, 2);
|
|
108
|
+
assert.equal(runCollector([t2.id, root], { GOTRY_HANDOFF_PLANNER_BIN: failPlanner }).status, 2);
|
|
138
109
|
assert.equal(calls().length, before);
|
|
139
110
|
}
|
|
111
|
+
// 6) 空输出 → failed
|
|
140
112
|
const t3 = await writeTurnHandoffTicket(root, '空输出的规划');
|
|
141
113
|
{
|
|
142
|
-
const r = runCollector([
|
|
143
|
-
t3.id,
|
|
144
|
-
root
|
|
145
|
-
], {
|
|
146
|
-
GOTRY_HANDOFF_PLANNER_BIN: emptyPlanner
|
|
147
|
-
});
|
|
114
|
+
const r = runCollector([t3.id, root], { GOTRY_HANDOFF_PLANNER_BIN: emptyPlanner });
|
|
148
115
|
assert.equal(r.status, 2);
|
|
149
116
|
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
150
117
|
assert.equal(terminal['error'], 'planner produced empty output');
|
|
151
118
|
}
|
|
119
|
+
// 7) --all:只收 open(t4/t5),已终态的 t1/t2/t3 零重算。
|
|
120
|
+
// t4/t5 同毫秒创建会让「最新优先」断言 flaky(requestedAt 毫秒精度);
|
|
121
|
+
// 把 t5 钉到 now+5s,排序严格确定。
|
|
152
122
|
const t4 = await writeTurnHandoffTicket(root, '批量规划一');
|
|
153
123
|
const t5 = await writeTurnHandoffTicket(root, '批量规划二');
|
|
154
124
|
{
|
|
@@ -156,50 +126,38 @@ try {
|
|
|
156
126
|
const t5json = JSON.parse(readFileSync(join(dir, `${t5.id}.json`), 'utf8'));
|
|
157
127
|
writeFileSync(join(dir, `${t5.id}.json`), JSON.stringify({
|
|
158
128
|
...t5json,
|
|
159
|
-
requestedAt: new Date(Date.now() + 5_000).toISOString()
|
|
129
|
+
requestedAt: new Date(Date.now() + 5_000).toISOString(),
|
|
160
130
|
}));
|
|
161
131
|
}
|
|
162
132
|
{
|
|
163
133
|
const before = calls().length;
|
|
164
|
-
const r = runCollector([
|
|
165
|
-
'--all',
|
|
166
|
-
root
|
|
167
|
-
], {
|
|
168
|
-
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
169
|
-
});
|
|
134
|
+
const r = runCollector(['--all', root], { GOTRY_HANDOFF_PLANNER_BIN: okPlanner });
|
|
170
135
|
assert.equal(r.status, 0, `stderr=${r.stderr}`);
|
|
171
136
|
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
172
137
|
assert.equal(terminal['status'], 'succeeded');
|
|
173
138
|
assert.equal(calls().length, before + 2, 'exactly the two open tickets collected');
|
|
174
139
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
], {
|
|
179
|
-
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
180
|
-
}).status, 0);
|
|
140
|
+
// 8) --all 无 open → exit 0
|
|
141
|
+
assert.equal(runCollector(['--all', root], { GOTRY_HANDOFF_PLANNER_BIN: okPlanner }).status, 0);
|
|
142
|
+
// 9) 复访视图:倒序 + settled 摘录 + failed 错误
|
|
181
143
|
{
|
|
182
144
|
const views = await listTurnHandoffTickets(root);
|
|
183
145
|
assert.equal(views.length, 5);
|
|
184
146
|
assert.equal(views[0].id, t5.id, 'newest first');
|
|
185
|
-
const settled = views.find(
|
|
147
|
+
const settled = views.find(v => v.id === t1.id);
|
|
186
148
|
assert.equal(settled.status, 'settled');
|
|
187
149
|
assert.ok(settled.deliverableExcerpt?.includes('方案A'));
|
|
188
|
-
const failed = views.find(
|
|
150
|
+
const failed = views.find(v => v.id === t2.id);
|
|
189
151
|
assert.equal(failed.status, 'failed');
|
|
190
152
|
assert.ok(failed.error?.startsWith('planner exited 3'));
|
|
191
153
|
}
|
|
192
154
|
console.log('turn handoff collector tests: OK (settle/fail/empty/idempotent replay/--all/recursion guard env/revisit views)');
|
|
193
|
-
} finally{
|
|
194
|
-
rmSync(root, {
|
|
195
|
-
recursive: true,
|
|
196
|
-
force: true
|
|
197
|
-
});
|
|
198
|
-
rmSync(planners, {
|
|
199
|
-
recursive: true,
|
|
200
|
-
force: true
|
|
201
|
-
});
|
|
202
155
|
}
|
|
156
|
+
finally {
|
|
157
|
+
rmSync(root, { recursive: true, force: true });
|
|
158
|
+
rmSync(planners, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
// settleTurnHandoffTicket 直接契约(不经 collector):原子写 + 字段完整
|
|
203
161
|
{
|
|
204
162
|
const r = mkdtempSync(join(tmpdir(), 'gotry-handoff-settle-'));
|
|
205
163
|
const t = await writeTurnHandoffTicket(r, '直调结算');
|
|
@@ -207,10 +165,5 @@ try {
|
|
|
207
165
|
assert.ok(existsSync(deliverablePath));
|
|
208
166
|
const views = await listTurnHandoffTickets(r);
|
|
209
167
|
assert.equal(views[0].deliverableExcerpt, '交付内容');
|
|
210
|
-
rmSync(r, {
|
|
211
|
-
recursive: true,
|
|
212
|
-
force: true
|
|
213
|
-
});
|
|
168
|
+
rmSync(r, { recursive: true, force: true });
|
|
214
169
|
}
|
|
215
|
-
|
|
216
|
-
//# sourceURL=ts/scripts/turn-handoff-collect-tests.ts
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后台深度规划工单收集器(ADR-24 v2 收集闭环;async-collect 的 turn-handoff 对位)。
|
|
3
|
+
*
|
|
4
|
+
* 「一小时后回来看看」的后台半段:加载 open 工单 → 以 GOTRY_HANDOFF_CHILD=1
|
|
5
|
+
* 派生 headless 规划会话(唯一出口 converge,不回递归)→ 捕获最终答复为
|
|
6
|
+
* 交付物 → settleTurnHandoffTicket 结算(交付物 .deliverable.md + 工单状态
|
|
7
|
+
* 原子更新)。终态幂等:已 settled/failed 的工单复诵交付物与终态 JSON,零
|
|
8
|
+
* 重算、零再花。最后一行固定输出 gotry_turn_handoff_terminal.v1 JSON;
|
|
9
|
+
* succeeded exit 0,failed exit 2。
|
|
10
|
+
*
|
|
11
|
+
* 运行(在 ts/ 下):
|
|
12
|
+
* npx tsx scripts/turn-handoff-collect.ts <ticketId> [stateRoot]
|
|
13
|
+
* npx tsx scripts/turn-handoff-collect.ts --all [stateRoot]
|
|
14
|
+
* 环境变量:
|
|
15
|
+
* GOTRY_HANDOFF_PLANNER_BIN 规划器二进制(默认 <repo>/bin/gotry-inner.js)
|
|
16
|
+
* GOTRY_HANDOFF_PLANNER_TIMEOUT_MS 规划器硬上限(默认 900_000)
|
|
17
|
+
*
|
|
18
|
+
* v1 诚实边界:工单只携带用户原文,规划会话是全新上下文(fresh cwd/DSH_HOME)
|
|
19
|
+
* ——原会话中已读的工作区文件/日历结论不随单迁移,由规划器按需重取。
|
|
20
|
+
*/
|
|
1
21
|
import { spawn } from 'node:child_process';
|
|
2
22
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
23
|
import { tmpdir } from 'node:os';
|
|
4
24
|
import { join } from 'node:path';
|
|
5
|
-
import { listTurnHandoffTickets, settleTurnHandoffTicket, TURN_HANDOFF_SCHEMA } from '../src/turn-deadline.js';
|
|
25
|
+
import { listTurnHandoffTickets, settleTurnHandoffTicket, TURN_HANDOFF_SCHEMA, } from '../src/turn-deadline.js';
|
|
6
26
|
const TURN_HANDOFF_TERMINAL_SCHEMA = 'gotry_turn_handoff_terminal.v1';
|
|
7
27
|
const ROOT = join(import.meta.dirname, '..', '..');
|
|
8
28
|
function emitTerminal(outcome) {
|
|
@@ -14,7 +34,8 @@ function loadTicket(stateRoot, ticketId) {
|
|
|
14
34
|
const path = join(stateRoot === '.' ? process.cwd() : stateRoot, 'gotry-state', 'turn-handoffs', `${ticketId}.json`);
|
|
15
35
|
const ticket = JSON.parse(readFileSync(path, 'utf-8'));
|
|
16
36
|
return ticket?.schema === TURN_HANDOFF_SCHEMA ? ticket : null;
|
|
17
|
-
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
18
39
|
return null;
|
|
19
40
|
}
|
|
20
41
|
}
|
|
@@ -24,105 +45,53 @@ function replay(ticket, stateRoot) {
|
|
|
24
45
|
schema: TURN_HANDOFF_TERMINAL_SCHEMA,
|
|
25
46
|
ticket_id: ticket.id,
|
|
26
47
|
status: ticket.status === 'settled' ? 'succeeded' : 'failed',
|
|
27
|
-
...ticket.deliverableFile ? {
|
|
28
|
-
|
|
29
|
-
} : {},
|
|
30
|
-
...ticket.error ? {
|
|
31
|
-
error: ticket.error
|
|
32
|
-
} : {}
|
|
48
|
+
...(ticket.deliverableFile ? { deliverable_path: join(dir, ticket.deliverableFile) } : {}),
|
|
49
|
+
...(ticket.error ? { error: ticket.error } : {}),
|
|
33
50
|
});
|
|
34
51
|
}
|
|
35
52
|
async function runPlanner(ticket) {
|
|
36
53
|
const plannerBin = process.env.GOTRY_HANDOFF_PLANNER_BIN ?? join(ROOT, 'bin', 'gotry-inner.js');
|
|
37
54
|
if (!existsSync(plannerBin)) {
|
|
38
|
-
return {
|
|
39
|
-
ok: false,
|
|
40
|
-
output: '',
|
|
41
|
-
error: `planner binary unavailable: ${plannerBin}`
|
|
42
|
-
};
|
|
55
|
+
return { ok: false, output: '', error: `planner binary unavailable: ${plannerBin}` };
|
|
43
56
|
}
|
|
44
57
|
const timeoutMs = Number.parseInt(process.env.GOTRY_HANDOFF_PLANNER_TIMEOUT_MS ?? '', 10) || 900_000;
|
|
45
58
|
const dshHome = mkdtempSync(join(tmpdir(), 'gotry-handoff-child-dsh-'));
|
|
46
59
|
const childCwd = mkdtempSync(join(tmpdir(), 'gotry-handoff-child-cwd-'));
|
|
47
60
|
try {
|
|
48
|
-
const childEnv = {
|
|
49
|
-
...process.env
|
|
50
|
-
};
|
|
61
|
+
const childEnv = { ...process.env };
|
|
51
62
|
childEnv.DSH_HOME = dshHome;
|
|
63
|
+
// 递归防护(核心):子会话唯一出口 converge + 长 leash;同时清掉父进程的
|
|
64
|
+
// 数值 pin 与工单根,子会话不得再 handoff、也不受测试阈值影响。
|
|
52
65
|
childEnv.GOTRY_HANDOFF_CHILD = '1';
|
|
53
66
|
delete childEnv.GOTRY_TURN_DEADLINE_SOFT_MS;
|
|
54
67
|
delete childEnv.GOTRY_TURN_DEADLINE_HARD_MS;
|
|
55
68
|
delete childEnv.GOTRY_TURN_HANDOFF_ROOT;
|
|
69
|
+
// .js/.mjs/.cjs 经当前 node 派生(测试假 planner 无执行位也成立);
|
|
70
|
+
// 其余(gotry-inner.js 之外的 .bin shim、全局命令)直接执行。
|
|
56
71
|
const isJsScript = /\.(js|mjs|cjs)$/.test(plannerBin);
|
|
57
|
-
const child = isJsScript
|
|
58
|
-
plannerBin,
|
|
59
|
-
ticket.userMessage
|
|
60
|
-
], {
|
|
61
|
-
cwd: childCwd,
|
|
62
|
-
env: childEnv,
|
|
63
|
-
stdio: [
|
|
64
|
-
'ignore',
|
|
65
|
-
'pipe',
|
|
66
|
-
'pipe'
|
|
67
|
-
]
|
|
68
|
-
}) : spawn(plannerBin, [
|
|
69
|
-
ticket.userMessage
|
|
70
|
-
], {
|
|
71
|
-
cwd: childCwd,
|
|
72
|
-
env: childEnv,
|
|
73
|
-
stdio: [
|
|
74
|
-
'ignore',
|
|
75
|
-
'pipe',
|
|
76
|
-
'pipe'
|
|
77
|
-
]
|
|
78
|
-
});
|
|
72
|
+
const child = isJsScript
|
|
73
|
+
? spawn(process.execPath, [plannerBin, ticket.userMessage], { cwd: childCwd, env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
74
|
+
: spawn(plannerBin, [ticket.userMessage], { cwd: childCwd, env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
79
75
|
let output = '';
|
|
80
|
-
child.stdout.on('data',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
child.stdout.on('data', chunk => { output += chunk.toString(); });
|
|
77
|
+
child.stderr.on('data', chunk => { output += chunk.toString(); });
|
|
78
|
+
const code = await new Promise(resolve => {
|
|
79
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); resolve(null); }, timeoutMs);
|
|
80
|
+
child.once('exit', c => { clearTimeout(timer); resolve(c); });
|
|
85
81
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}, timeoutMs);
|
|
91
|
-
child.once('exit', (c)=>{
|
|
92
|
-
clearTimeout(timer);
|
|
93
|
-
resolve(c);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
if (code === null) return {
|
|
97
|
-
ok: false,
|
|
98
|
-
output: '',
|
|
99
|
-
error: `planner timed out after ${timeoutMs}ms`
|
|
100
|
-
};
|
|
101
|
-
if (code !== 0) return {
|
|
102
|
-
ok: false,
|
|
103
|
-
output: '',
|
|
104
|
-
error: `planner exited ${code}: ${output.trim().slice(-300)}`
|
|
105
|
-
};
|
|
82
|
+
if (code === null)
|
|
83
|
+
return { ok: false, output: '', error: `planner timed out after ${timeoutMs}ms` };
|
|
84
|
+
if (code !== 0)
|
|
85
|
+
return { ok: false, output: '', error: `planner exited ${code}: ${output.trim().slice(-300)}` };
|
|
106
86
|
const deliverable = output.trim();
|
|
107
|
-
if (!deliverable)
|
|
108
|
-
ok: false,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
};
|
|
116
|
-
} finally{
|
|
117
|
-
await new Promise((resolve)=>setTimeout(resolve, 200));
|
|
118
|
-
rmSync(dshHome, {
|
|
119
|
-
recursive: true,
|
|
120
|
-
force: true
|
|
121
|
-
});
|
|
122
|
-
rmSync(childCwd, {
|
|
123
|
-
recursive: true,
|
|
124
|
-
force: true
|
|
125
|
-
});
|
|
87
|
+
if (!deliverable)
|
|
88
|
+
return { ok: false, output: '', error: 'planner produced empty output' };
|
|
89
|
+
return { ok: true, output: deliverable };
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
93
|
+
rmSync(dshHome, { recursive: true, force: true });
|
|
94
|
+
rmSync(childCwd, { recursive: true, force: true });
|
|
126
95
|
}
|
|
127
96
|
}
|
|
128
97
|
async function collectOne(stateRoot, ticketId) {
|
|
@@ -136,41 +105,45 @@ async function collectOne(stateRoot, ticketId) {
|
|
|
136
105
|
replay(ticket, stateRoot);
|
|
137
106
|
}
|
|
138
107
|
const result = await runPlanner(ticket);
|
|
139
|
-
const deliverable = result.ok
|
|
108
|
+
const deliverable = result.ok
|
|
109
|
+
? `# 后台深度规划交付:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n${result.output}`
|
|
110
|
+
: `# 后台深度规划失败:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n未产出交付物:${result.error ?? '未知原因'}。这趟规划没有完成——请重新发起,失败原因如上,无部分结果可交付。`;
|
|
140
111
|
const { deliverablePath } = await settleTurnHandoffTicket(stateRoot, ticket, result.ok ? 'settled' : 'failed', deliverable, result.ok ? undefined : result.error);
|
|
141
112
|
console.error(`工单 ${ticket.id} ${result.ok ? '已交付' : '失败'}:${deliverablePath}`);
|
|
142
113
|
emitTerminal({
|
|
143
114
|
schema: TURN_HANDOFF_TERMINAL_SCHEMA,
|
|
144
115
|
ticket_id: ticket.id,
|
|
145
116
|
status: result.ok ? 'succeeded' : 'failed',
|
|
146
|
-
...result.ok
|
|
147
|
-
deliverable_path: deliverablePath
|
|
148
|
-
|
|
149
|
-
error: result.error ?? 'unknown'
|
|
150
|
-
}
|
|
117
|
+
...(result.ok
|
|
118
|
+
? { deliverable_path: deliverablePath }
|
|
119
|
+
: { error: result.error ?? 'unknown' }),
|
|
151
120
|
});
|
|
152
121
|
}
|
|
153
122
|
async function collectAll(stateRoot) {
|
|
154
|
-
const open = (await listTurnHandoffTickets(stateRoot)).filter(
|
|
123
|
+
const open = (await listTurnHandoffTickets(stateRoot)).filter(t => t.status === 'open');
|
|
155
124
|
if (open.length === 0) {
|
|
156
125
|
console.error('无待回收的后台规划工单(open=0)');
|
|
157
126
|
process.exit(0);
|
|
158
127
|
}
|
|
159
128
|
let failed = 0;
|
|
160
|
-
for (const view of open){
|
|
129
|
+
for (const view of open) {
|
|
161
130
|
const ticket = loadTicket(stateRoot, view.id);
|
|
162
|
-
if (!ticket || ticket.status !== 'open')
|
|
131
|
+
if (!ticket || ticket.status !== 'open')
|
|
132
|
+
continue;
|
|
163
133
|
const result = await runPlanner(ticket);
|
|
164
|
-
const deliverable = result.ok
|
|
134
|
+
const deliverable = result.ok
|
|
135
|
+
? `# 后台深度规划交付:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n${result.output}`
|
|
136
|
+
: `# 后台深度规划失败:${ticket.objective.slice(0, 60)}\n(工单 ${ticket.id},原请求 ${ticket.requestedAt})\n\n未产出交付物:${result.error ?? '未知原因'}。这趟规划没有完成——请重新发起,失败原因如上,无部分结果可交付。`;
|
|
165
137
|
const { deliverablePath } = await settleTurnHandoffTicket(stateRoot, ticket, result.ok ? 'settled' : 'failed', deliverable, result.ok ? undefined : result.error);
|
|
166
138
|
console.error(`工单 ${ticket.id} ${result.ok ? '已交付' : '失败'}:${deliverablePath}`);
|
|
167
|
-
if (!result.ok)
|
|
139
|
+
if (!result.ok)
|
|
140
|
+
failed += 1;
|
|
168
141
|
}
|
|
169
142
|
console.log(JSON.stringify({
|
|
170
143
|
schema: TURN_HANDOFF_TERMINAL_SCHEMA,
|
|
171
144
|
ticket_id: `--all(${open.length})`,
|
|
172
145
|
status: failed === 0 ? 'succeeded' : 'failed',
|
|
173
|
-
error: failed === 0 ? undefined : `${failed}/${open.length}
|
|
146
|
+
error: failed === 0 ? undefined : `${failed}/${open.length} 张工单回收失败`,
|
|
174
147
|
}));
|
|
175
148
|
process.exit(failed === 0 ? 0 : 2);
|
|
176
149
|
}
|
|
@@ -178,12 +151,11 @@ const [target, stateRootArg] = process.argv.slice(2);
|
|
|
178
151
|
const stateRoot = stateRootArg ?? '.';
|
|
179
152
|
if (target === '--all') {
|
|
180
153
|
await collectAll(stateRoot);
|
|
181
|
-
}
|
|
154
|
+
}
|
|
155
|
+
else if (target) {
|
|
182
156
|
await collectOne(stateRoot, target);
|
|
183
|
-
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
184
159
|
console.error('用法:npx tsx scripts/turn-handoff-collect.ts <ticketId|--all> [stateRoot]');
|
|
185
160
|
process.exit(1);
|
|
186
161
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//# sourceURL=ts/scripts/turn-handoff-collect.ts
|