@whyshu/argent-engine 0.0.0-stage → 0.6.1
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/MANIFEST.json +341 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +378 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/collect-evidence.mjs +42 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +135 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +47 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +261 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/final-check.mjs +43 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1025 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2278 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +195 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent-datadir.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +137 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +74 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +35 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +738 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-bundled-migrators.mjs — 校验**打包副本**里的迁移器/校验器也带上了 hotfix。
|
|
4
|
+
*
|
|
5
|
+
* 由来(2026-09-29 事故,实例 session-06732069「Argent项目」):
|
|
6
|
+
* `@deepseek-ai/dsh-session-persistence-jsonl` 在做 v3→v4 迁移时,会另起一个
|
|
7
|
+
* **worker 线程**对迁移产物做最终校验,而那个 worker 是**打包件**
|
|
8
|
+
* (`lib/worker.cjs`,~540KB,内联了一整份 v0→v1 与 v3→v4 的迁移器/校验器)。
|
|
9
|
+
* `scripts/patches/*.patch` 历来只打在 `lib/index.js` 这些**模块**上 ⇒
|
|
10
|
+
* · `verify-session-migration.mjs`(import 模块)**一直假绿**;
|
|
11
|
+
* · 真实 resume 走 worker 的旧副本 → `SessionPersistenceCorruptionError:
|
|
12
|
+
* stored log is corrupt: Error: tool/result is outside an open turn`
|
|
13
|
+
* ⇒ 前端发消息弹 toast、会话打不开。
|
|
14
|
+
*
|
|
15
|
+
* 判据(文本级,诚实说明边界):worker 是打包件、无导出,无法直接做行为断言;
|
|
16
|
+
* 这里用**紧贴语义的代码形状**断言两处 hotfix 是否在打包副本里:
|
|
17
|
+
* A. v3→v4 `tool()`:turn 外的替换型 tool/result 必须走"校验 data.turn 指向已关闭
|
|
18
|
+
* 历史 turn"的放宽分支,**不得**再是裸 `this.requireTurn(event.type);`。
|
|
19
|
+
* B. v0 `permission/preset`:必须容忍旧数据里的 `origin` 键。
|
|
20
|
+
* 任一不满足 → exit 1,提示先跑 `node scripts/dsh-upgrade.mjs --patches-only`。
|
|
21
|
+
*
|
|
22
|
+
* 用法:node scripts/patches/verify-bundled-migrators.mjs
|
|
23
|
+
* 退出码:0 通过(或本机无该打包件,跳过)/ 1 失守
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
26
|
+
import { requireDshScope } from '../lib/dsh-scope.mjs'
|
|
27
|
+
|
|
28
|
+
// 0.6.0 F1 修复批(2026-10-14):探不到 scope ⇒ 响亮失败(⛔ 不用假路径 ⇒ 否则本脚本会
|
|
29
|
+
// "本机无该打包件,跳过"地**假绿** —— 而它正是防"worker 打包副本漏打 hotfix"的那道门禁)。
|
|
30
|
+
let SCOPE
|
|
31
|
+
try { SCOPE = requireDshScope({ what: '打包副本校验(verify-bundled-migrators)' }) }
|
|
32
|
+
catch (e) { console.error(` ❌ ${e.message}`); process.exit(1) }
|
|
33
|
+
const WORKER = `${SCOPE}/dsh-session-persistence-jsonl/lib/worker.cjs`
|
|
34
|
+
|
|
35
|
+
let pass = 0, fail = 0
|
|
36
|
+
const ok = (c, n) => { c ? (pass++, console.log(` ✅ ${n}`)) : (fail++, console.log(` ❌ ${n}`)) }
|
|
37
|
+
|
|
38
|
+
console.log('【打包副本】dsh-session-persistence-jsonl/lib/worker.cjs 的迁移器 hotfix')
|
|
39
|
+
|
|
40
|
+
if (!existsSync(WORKER)) {
|
|
41
|
+
console.log(` ⏭ 本机没有该打包件(${WORKER})——跳过(上游可能改了实现)`)
|
|
42
|
+
process.exit(0)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const src = readFileSync(WORKER, 'utf8')
|
|
46
|
+
|
|
47
|
+
// ── A. v3→v4 tool():放宽 turn 外的替换型 tool/result ──────────────────────────
|
|
48
|
+
const toolAt = src.indexOf('\ttool(event, data) {')
|
|
49
|
+
if (toolAt === -1) {
|
|
50
|
+
ok(false, 'worker 里能找到 v3→v4 的 tool() 方法(上游结构可能已变,本门禁需同步)')
|
|
51
|
+
} else {
|
|
52
|
+
// 取该方法开头到 `this.requireStep(` 之间的片段(即 surfaceOp !== "append" 分支)
|
|
53
|
+
const stepAt = src.indexOf('this.requireStep(event, data);', toolAt)
|
|
54
|
+
const branch = stepAt === -1 ? src.slice(toolAt, toolAt + 1200) : src.slice(toolAt, stepAt)
|
|
55
|
+
ok(/Hardening \(2026-09-29, argent-dsh hotfix, mirrored into the bundled worker\)/.test(branch),
|
|
56
|
+
'A1 打补丁标记存在(mirrored into the bundled worker)')
|
|
57
|
+
ok(!/^\s*this\.requireTurn\(event\.type\);\s*$/m.test(branch),
|
|
58
|
+
'A2 已不再是裸 requireTurn(否则 turn 外替换型 tool/result 会否掉整个迁移)')
|
|
59
|
+
ok(/const t = data\["turn"\];/.test(branch) && /t >= this\.nextTurn/.test(branch),
|
|
60
|
+
'A3 放宽分支在校验 data.turn 指向已关闭的历史 turn(不是无条件放行)')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── B. v0 permission/preset:容忍 origin ────────────────────────────────────
|
|
64
|
+
ok(/"permission\/preset": disposition\(\["preset"\], \["origin"\]\)/.test(src),
|
|
65
|
+
'B1 v0 的 permission/preset 容忍旧数据的 origin 键(2026-09-10 hotfix 的镜像)')
|
|
66
|
+
ok(!/"permission\/preset": disposition\(\["preset"\]\),/.test(src),
|
|
67
|
+
'B2 旧的严格 disposition 已不存在')
|
|
68
|
+
|
|
69
|
+
// ── C. 共享的 requireTurn 仍应保持严格(放宽只针对替换型 tool/result)─────────
|
|
70
|
+
ok(/requireTurn\(type\) \{\s*\n\s*if \(this\.turn === null\) throw new SessionFormatError\(`\$\{type\} is outside an open turn`\);/.test(src),
|
|
71
|
+
'C1 requireTurn 本身仍是严格的(放宽没有波及别的下游事件类型)')
|
|
72
|
+
|
|
73
|
+
console.log(`\n── 合计:通过 ${pass} / 失败 ${fail}`)
|
|
74
|
+
if (fail > 0) {
|
|
75
|
+
console.log('❌ 打包副本未同步 hotfix —— 真实 resume 会否掉会话(前端发送消息报 toast)')
|
|
76
|
+
console.log(' 修法:node scripts/dsh-upgrade.mjs --patches-only (必须重启前跑)')
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
console.log('✅ 打包副本的迁移器 hotfix 已同步(模块 + worker 两侧一致)')
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 加固补丁校验(2026-09-18 会话毒化事故的防线)。
|
|
4
|
+
*
|
|
5
|
+
* 两个补丁:
|
|
6
|
+
* A. dsh-tools--render-return-shape.patch
|
|
7
|
+
* `createSuccessResult()` 把 output.render 的返回值**直接**当作 tool-result content。
|
|
8
|
+
* 补丁把非数组返回值规范化成 [{type:'text',text}] 并 warn 点名工具。
|
|
9
|
+
* B. dsh-session--append-message-shape.patch
|
|
10
|
+
* `Session.append()` 原本只做 validateSessionEventData + surface 校验,**漏了
|
|
11
|
+
* assertMessageEventShape** ⇒ 一个非法 message 事件能写进日志,直到**下次从磁盘加载**
|
|
12
|
+
* 才炸("message must contain one tool-result block"),此时会话根本打不开,
|
|
13
|
+
* surface replace 也救不回来(校验针对事件本身,不是 surface 视图)。
|
|
14
|
+
*
|
|
15
|
+
* 本脚本对 B 做**真实行为验证**(真造一个会话、真 append),对 A 做源码语义断言
|
|
16
|
+
* (ToolRuntime 需要完整 ctx,其端到端验证在运行中的实例里用动态插件完成)。
|
|
17
|
+
*
|
|
18
|
+
* 用法:node verify-hardening.mjs (只读 + 内存会话,不触碰任何 session 文件)
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
21
|
+
import { requireDshScope } from '../lib/dsh-scope.mjs'
|
|
22
|
+
|
|
23
|
+
// 2026-09-28:scope 解析改为共享实现(客户机 dsh 在 ~/node/lib/node_modules,
|
|
24
|
+
// 写死 homebrew 会让校验在客户机上一律假红 —— 实测把升级门禁整片挡住)。
|
|
25
|
+
// 0.6.0 F1 修复批(2026-10-14):**探不到**(null / 空壳)⇒ 响亮失败;⛔ 不许拿假路径继续 ——
|
|
26
|
+
// 那会让下面 `existsSync(TOOLS)` 恒 false 而"降级"成跳过 ⇒ 门禁**假绿**(看着通过、其实没验)。
|
|
27
|
+
let SCOPE
|
|
28
|
+
try { SCOPE = requireDshScope({ what: '补丁校验(verify-hardening)' }) }
|
|
29
|
+
catch (e) { console.error(` ❌ ${e.message}`); process.exit(1) }
|
|
30
|
+
const TOOLS = `${SCOPE}/dsh-tools/lib/index.js`
|
|
31
|
+
const SESSION = `${SCOPE}/dsh-session/lib/index.js`
|
|
32
|
+
|
|
33
|
+
let pass = 0, fail = 0
|
|
34
|
+
const ok = (c, n) => { c ? (pass++, console.log(` ✅ ${n}`)) : (fail++, console.log(` ❌ ${n}`)) }
|
|
35
|
+
|
|
36
|
+
console.log('【补丁 A】dsh-tools:output.render 返回值规范化')
|
|
37
|
+
if (!existsSync(TOOLS)) { ok(false, `dsh-tools 存在(${TOOLS})`) }
|
|
38
|
+
else {
|
|
39
|
+
const src = readFileSync(TOOLS, 'utf8')
|
|
40
|
+
ok(/function normalizeRenderOutput\(toolName, rendered\)/.test(src), 'normalizeRenderOutput() 已定义')
|
|
41
|
+
ok(/snapshotProjection\(tool\.name, "render", normalizeRenderOutput\(tool\.name, rendered\)\)/.test(src),
|
|
42
|
+
'createSuccessResult() 已经过它规范化 render 返回值')
|
|
43
|
+
ok(/if \(Array\.isArray\(rendered\)\) return rendered;/.test(src), '数组返回值原样透传(不改变正常工具行为)')
|
|
44
|
+
|
|
45
|
+
// 行为验证:把函数体取出来直接调用(补丁把它插在 defineTool 之前,故以下一个顶层函数为界)
|
|
46
|
+
const start = src.indexOf('function normalizeRenderOutput(')
|
|
47
|
+
const end = src.indexOf('\nfunction defineTool(', start)
|
|
48
|
+
const body = start === -1 || end === -1 ? '' : src.slice(start, end)
|
|
49
|
+
let normalize = null
|
|
50
|
+
try { normalize = new Function(`${body}\nreturn normalizeRenderOutput`)() } catch { /* 保持 null */ }
|
|
51
|
+
ok(typeof normalize === 'function', 'normalizeRenderOutput 可独立求值(用于行为断言)')
|
|
52
|
+
if (typeof normalize === 'function') {
|
|
53
|
+
const originalWarn = console.warn
|
|
54
|
+
const warnings = []
|
|
55
|
+
console.warn = (msg) => warnings.push(String(msg))
|
|
56
|
+
let cases
|
|
57
|
+
try {
|
|
58
|
+
cases = {
|
|
59
|
+
passThrough: normalize('probe', [{ type: 'text', text: 'ok' }]),
|
|
60
|
+
fromString: normalize('probe', '流程 full:无未处置失败'),
|
|
61
|
+
fromObject: normalize('probe', { count: 2 }),
|
|
62
|
+
fromUndefined: normalize('probe', undefined),
|
|
63
|
+
fromNull: normalize('probe', null),
|
|
64
|
+
}
|
|
65
|
+
} finally { console.warn = originalWarn }
|
|
66
|
+
const isBlocks = (v) => Array.isArray(v) && v.length > 0
|
|
67
|
+
&& v.every((b) => b && typeof b === 'object' && typeof b.type === 'string' && typeof b.text === 'string')
|
|
68
|
+
ok(Array.isArray(cases.passThrough) && cases.passThrough[0].text === 'ok', '数组原样返回(不复制、不改写)')
|
|
69
|
+
ok(isBlocks(cases.fromString) && cases.fromString[0].text === '流程 full:无未处置失败',
|
|
70
|
+
`毒源场景:字符串被包装成 ContentBlock(${JSON.stringify(cases.fromString)})`)
|
|
71
|
+
ok(isBlocks(cases.fromObject) && cases.fromObject[0].text === '{"count":2}', '对象被 JSON 化后包装')
|
|
72
|
+
ok(isBlocks(cases.fromUndefined) && cases.fromUndefined[0].text === 'undefined', 'undefined 被安全包装(不会写成 content: undefined)')
|
|
73
|
+
ok(isBlocks(cases.fromNull), 'null 被安全包装')
|
|
74
|
+
ok(warnings.length === 4 && warnings.every((w) => w.includes('"probe"') && w.includes('output.render must return a ContentBlock array')),
|
|
75
|
+
`每次都 warn 并点名工具(${warnings.length} 条)`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('\n【补丁 B】dsh-session:append 边界补上 message 形状校验')
|
|
80
|
+
if (!existsSync(SESSION)) { ok(false, `dsh-session 存在(${SESSION})`) }
|
|
81
|
+
else {
|
|
82
|
+
const src = readFileSync(SESSION, 'utf8')
|
|
83
|
+
ok(/Hardening \(2026-09-18, argent-dsh hotfix\)/.test(src), '打补丁标记存在')
|
|
84
|
+
const appendBody = src.slice(src.indexOf('\tappend(type, data, ...opts) {'), src.indexOf('headerFold;', src.indexOf('\tappend(type, data, ...opts) {')))
|
|
85
|
+
ok(/validateSessionEventData\(event, `session event "\$\{type\}" at seq \$\{event\.seq\}`\);\s*\n\s*\/\* Hardening[\s\S]*?\*\/\s*\n\s*assertMessageEventShape\(event, `session event "\$\{type\}" at seq \$\{event\.seq\}`\);/.test(appendBody),
|
|
86
|
+
'append() 在落盘前调用 assertMessageEventShape()')
|
|
87
|
+
|
|
88
|
+
// 真实行为验证:真造会话、真 append
|
|
89
|
+
const { Session } = await import(SESSION)
|
|
90
|
+
const session = Session.create('verify-hardening-probe')
|
|
91
|
+
// ⚠️ 版本差异(2026-09-29 实测):0.1.5 的 `tool/result` 容忍 message.role='user',
|
|
92
|
+
// 0.1.7 起收紧为必须 'tool'(dsh 自己的 MESSAGE_ROLE_BY_TYPE 校验)。
|
|
93
|
+
// 夹具按目标版本自适应:先试 'tool',抛"role"错再退回 'user'。
|
|
94
|
+
// ⚠️ 夹具形态修正(2026-09-29):`mkToolResult` 的参数是 **message.content 本身**。
|
|
95
|
+
// 0.1.7 真实写入路径 = dsh-llm `createToolResultMessage({content})` 把 render 输出
|
|
96
|
+
// **直接**当 message.content(v4 形态,见 dsh-agent-loop `appendToolResult`);
|
|
97
|
+
// 旧夹具套了一层 v3 的 `{type:'tool-result',content}` wrapper —— 那是 v3 遗留形态
|
|
98
|
+
// (v3→v4 迁移器会把它展开,retired syntax),append 边界本就容忍,
|
|
99
|
+
// 拿它当"坏样本"只会得到**假红**(补丁 10:34 放宽规则后一直红着)。
|
|
100
|
+
const mkToolResult = (content, role) => ({
|
|
101
|
+
turn: 1,
|
|
102
|
+
step: 1,
|
|
103
|
+
message: {
|
|
104
|
+
id: 'verify-message-1',
|
|
105
|
+
role,
|
|
106
|
+
toolCallId: 'verify-call-1',
|
|
107
|
+
content,
|
|
108
|
+
source: { kind: 'tool', callId: 'verify-call-1' },
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
let role = 'tool'
|
|
113
|
+
let good = null
|
|
114
|
+
try { good = session.append('tool/result', mkToolResult([{ type: 'text', text: '正常工具结果' }], role), { surfaceOp: 'append' }) }
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (/must have role/i.test(err.message)) {
|
|
117
|
+
role = 'user'
|
|
118
|
+
try { good = session.append('tool/result', mkToolResult([{ type: 'text', text: '正常工具结果' }], role), { surfaceOp: 'append' }) }
|
|
119
|
+
catch (err2) { good = { error: err2.message } }
|
|
120
|
+
} else { good = { error: err.message } }
|
|
121
|
+
}
|
|
122
|
+
ok(good && good.error === undefined, `合法 tool-result 仍可写入(seq ${good && good.seq})`)
|
|
123
|
+
|
|
124
|
+
// 坏样本 ①:真毒源 —— render 返回裸字符串且补丁 A 未打时,message.content 就是字符串。
|
|
125
|
+
// (上游 0.1.7 自带 Array.isArray 会先拦;这断言保证这条线不会静默失守。)
|
|
126
|
+
let bad = null
|
|
127
|
+
try { session.append('tool/result', mkToolResult('裸字符串', role), { surfaceOp: 'append' }); bad = null }
|
|
128
|
+
catch (err) { bad = err.message }
|
|
129
|
+
ok(bad !== null && /invalid content|non-empty content block array/.test(bad),
|
|
130
|
+
`毒源 tool-result(content 为裸字符串)在**写入时**即被拒绝:${bad || '竟然写进去了!'}`)
|
|
131
|
+
|
|
132
|
+
// 坏样本 ②:空数组 —— 上游 Array.isArray 放行,**只有**补丁 B 的规则能拦住,
|
|
133
|
+
// 用它证明"我方加固规则确实在生效"(否则 ① 可能只是上游功劳)。
|
|
134
|
+
let empty = null
|
|
135
|
+
try { session.append('tool/result', mkToolResult([], role), { surfaceOp: 'append' }); empty = null }
|
|
136
|
+
catch (err) { empty = err.message }
|
|
137
|
+
ok(empty !== null && /must contain a non-empty content block array/.test(empty),
|
|
138
|
+
`空 content 数组在**写入时**即被拒绝(补丁 B 独立生效):${empty || '竟然写进去了!'}`)
|
|
139
|
+
|
|
140
|
+
let roleBad = null
|
|
141
|
+
try {
|
|
142
|
+
session.append('assistant/message', {
|
|
143
|
+
turn: 1, step: 1,
|
|
144
|
+
message: { id: 'verify-message-2', role: 'user', content: [{ type: 'text', text: 'x' }], source: { kind: 'model', provider: 'p', model: 'm' } },
|
|
145
|
+
stream: [],
|
|
146
|
+
}, { surfaceOp: 'append' })
|
|
147
|
+
roleBad = null
|
|
148
|
+
} catch (err) { roleBad = err.message }
|
|
149
|
+
ok(roleBad !== null && /must have role/.test(roleBad), `role 不符的 message 也被拒绝:${roleBad || '竟然写进去了!'}`)
|
|
150
|
+
|
|
151
|
+
let nonMessage = null
|
|
152
|
+
try { nonMessage = session.append('turn/start', { turn: 1 }) }
|
|
153
|
+
catch (err) { nonMessage = { error: err.message } }
|
|
154
|
+
ok(nonMessage && nonMessage.error === undefined, '非 message 事件(turn/start)不受影响,照常写入')
|
|
155
|
+
|
|
156
|
+
// 拒绝后日志不应增长(校验先于 this.log.push)
|
|
157
|
+
const before = session.seq
|
|
158
|
+
try { session.append('tool/result', mkToolResult('又一个裸字符串'), { surfaceOp: 'append' }) } catch { /* 预期 */ }
|
|
159
|
+
ok(session.seq === before, '被拒绝的事件**没有**进入日志(校验先于 push)')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`\n── 合计:通过 ${pass} / 失败 ${fail}`)
|
|
163
|
+
if (fail > 0) {
|
|
164
|
+
console.log('❌ 加固补丁校验未通过:补丁可能没打上或上游已改动 —— 先别重启,见 AGENTS.md「会话毒化事故」')
|
|
165
|
+
process.exit(1)
|
|
166
|
+
}
|
|
167
|
+
console.log('✅ 加固补丁校验通过:render 返回值被规范化 + append 边界拒绝无法重放的 message 事件')
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-session-migration.mjs — 对真实 session 文件跑完整格式迁移链(v0→v3)验证。
|
|
4
|
+
*
|
|
5
|
+
* 用途:dsh 升级后检查旧 session 兼容性(如 0.1.5 的 v0-to-v1 origin 遗漏 bug)。
|
|
6
|
+
* 用法:
|
|
7
|
+
* node verify-session-migration.mjs <session.jsonl.zstd> [...more]
|
|
8
|
+
* node verify-session-migration.mjs ~/.dsh/sessions/.../session-<id>/session.jsonl.zstd
|
|
9
|
+
*
|
|
10
|
+
* 依赖:全局 dsh scope(/opt/homebrew/lib/node_modules/@deepseek-ai)。
|
|
11
|
+
* 输出:每个文件 ✅/❌ + 事件数;全部通过 exit 0,任一失败 exit 1。
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from 'node:module'
|
|
14
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { execFileSync } from 'node:child_process'
|
|
16
|
+
import { tmpdir } from 'node:os'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { requireDshScope } from '../lib/dsh-scope.mjs'
|
|
19
|
+
import { decodeSessionLines, readSessionHeader } from '../lib/zstd-frames.mjs'
|
|
20
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
21
|
+
|
|
22
|
+
// 2026-09-10 修复:本脚本在**仓库目录**下运行,裸 specifier 无法从全局 scope 解析
|
|
23
|
+
// (原实现 `import ... from '@deepseek-ai/dsh-session-format-catalog'` 直接 ERR_MODULE_NOT_FOUND)。
|
|
24
|
+
// 改为锚定全局 scope 的 require(Node ≥22.12 的 require(esm) 可用),并允许 DSH_SCOPE 覆盖。
|
|
25
|
+
// 0.6.0 F1 修复批(2026-10-14):本脚本的 require 就锚在 scope 上 ⇒ 拿不到 scope 就**响亮失败**
|
|
26
|
+
// (⛔ 不用假路径 —— 那会 ERR_MODULE_NOT_FOUND,看着像"宿主坏了",其实是 scope 探错树)。
|
|
27
|
+
let _SCOPE_DIR
|
|
28
|
+
try { _SCOPE_DIR = requireDshScope({ what: '会话迁移校验(verify-session-migration)' }) }
|
|
29
|
+
catch (e) { console.error(` ❌ ${e.message}`); process.exit(1) }
|
|
30
|
+
const SCOPE = _SCOPE_DIR.replace(/\/?$/, '/')
|
|
31
|
+
const requireGlobal = createRequire(SCOPE)
|
|
32
|
+
const { sessionFormatCatalog, createSessionFormatCatalogWithChildren } = requireGlobal('@deepseek-ai/dsh-session-format-catalog')
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 收集某会话的**子会话事实**(v3→v4 迁移需要)。
|
|
36
|
+
*
|
|
37
|
+
* 为什么必须自己找(2026-09-29):0.1.7 新增 v3→v4 迁移,它要求父会话显式声明其历史子会话
|
|
38
|
+
* (含"没有子会话"时的空数组)。真实加载路径 `dsh-session-persistence-jsonl` 会扫
|
|
39
|
+
* `origin === 'subagent' && parentSession === <id>` 并把结果传进
|
|
40
|
+
* `createSessionFormatCatalogWithChildren()`;本校验器原先直接拿默认 catalog ⇒
|
|
41
|
+
* 报 "requires explicit historical child facts",**把"校验器绕过真实路径"误报成"旧会话打不开"**。
|
|
42
|
+
* 这里照真实路径的口径补上。
|
|
43
|
+
*/
|
|
44
|
+
function collectChildren(sessionId) {
|
|
45
|
+
const root = join(process.env.DSH_HOME || join(process.env.HOME || '', '.dsh'), 'sessions')
|
|
46
|
+
const out = []
|
|
47
|
+
let dirs = []
|
|
48
|
+
try {
|
|
49
|
+
for (const a of readdirSync(root)) {
|
|
50
|
+
for (const b of readdirSync(join(root, a))) {
|
|
51
|
+
const d = join(root, a, b)
|
|
52
|
+
// 2026-09-29 修复:对齐 dsh 的 listArtifacts——识别**所有生成文件**(含版本化
|
|
53
|
+
// session.v3/.v4.jsonl.zstd),每个目录选最高版本;此前只认未版本化文件名,
|
|
54
|
+
// 且证据缺 descriptorCount/descriptor ⇒ 误报 "requires historical child identity"。
|
|
55
|
+
let files = []
|
|
56
|
+
try { files = readdirSync(d).filter((f) => /^session(\.v\d+)?\.jsonl\.zstd$/.test(f)) } catch { continue }
|
|
57
|
+
if (files.length === 0) continue
|
|
58
|
+
const ver = (f) => { const m = /\.v(\d+)\./.exec(f); return m ? Number(m[1]) : 0 }
|
|
59
|
+
files.sort((x, y) => ver(y) - ver(x) || (x < y ? -1 : 1))
|
|
60
|
+
dirs.push(join(d, files[0]))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch { return out }
|
|
64
|
+
for (const f of dirs) {
|
|
65
|
+
const h = readSessionHeader(f)
|
|
66
|
+
if (!h || h.origin !== 'subagent' || h.parentSession !== sessionId) continue
|
|
67
|
+
// 提取 descriptor 证据(对齐 dsh historicalChildCatalogSource):descriptor 键必须存在;
|
|
68
|
+
// 读取失败 → 与 dsh 的 unavailable 兜底同款(descriptorCount: 0, descriptor: null)。
|
|
69
|
+
let descriptorCount = 0
|
|
70
|
+
let descriptor = null
|
|
71
|
+
try {
|
|
72
|
+
const { lines } = decodeSessionLines(readFileSync(f))
|
|
73
|
+
const descs = lines.map((l) => JSON.parse(l.raw)).filter((e) => e.type === 'subagent/descriptor')
|
|
74
|
+
descriptorCount = descs.length
|
|
75
|
+
descriptor = descs[0]?.data ?? null
|
|
76
|
+
} catch { /* 读失败 → 兜底 */ }
|
|
77
|
+
out.push({ childId: h.id, childCreatedAt: h.createdAt, descriptorCount, descriptor })
|
|
78
|
+
}
|
|
79
|
+
return out.sort((x, y) => x.childCreatedAt - y.childCreatedAt || (x.childId < y.childId ? -1 : 1))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const files = process.argv.slice(2)
|
|
83
|
+
if (files.length === 0) {
|
|
84
|
+
console.error('用法: node verify-session-migration.mjs <session.jsonl.zstd> [...]')
|
|
85
|
+
process.exit(2)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let failed = 0
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
// 解压:走共享的多帧切帧实现(lib/zstd-frames.mjs)。
|
|
91
|
+
// 2026-09-28 两个坑都在这里踩过:
|
|
92
|
+
// ① 原来只走 `zstd` CLI(本机 homebrew 有、客户机没装)⇒ 一律 "spawnSync zstd ENOENT";
|
|
93
|
+
// ② 改用 Node 内置 zstdDecompressSync ⇒ **只解首帧**(65MB 日志只出 182 字节),
|
|
94
|
+
// 校验会"通过"却什么都没看 —— 假绿比报错更危险。
|
|
95
|
+
const tmp = join(tmpdir(), `dsh-sess-verify-${process.pid}.jsonl`)
|
|
96
|
+
try {
|
|
97
|
+
const { frames } = decodeSessionLines(readFileSync(file))
|
|
98
|
+
if (frames.length === 0) throw new Error('切帧结果为空')
|
|
99
|
+
writeFileSync(tmp, Buffer.concat(frames.map((f) => f.plain)))
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.log(`❌ ${file}: 解压失败 ${e.message}`)
|
|
102
|
+
failed++
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ⚠️ 2026-09-29 修复「假红」:**不要**用 Node readline 切行。
|
|
107
|
+
// readline 把 U+2028/U+2029 也当行终止符,而真实加载路径
|
|
108
|
+
// (dsh-session-persistence-jsonl)只按**字节 10 (`\n`)** 切(`chunk.indexOf(10)`;
|
|
109
|
+
// U+2028 的 UTF-8 是 E2 80 A8,不含 0x0A)。实测 session-f82d039e 的 tool-result
|
|
110
|
+
// 文本里含 U+2028 ⇒ readline 把一行劈成两半,报 "Unterminated string in JSON",
|
|
111
|
+
// 把**能正常打开**的会话判成打不开。这里与真实路径统一口径:只按 '\n' 切。
|
|
112
|
+
const lines = readFileSync(tmp, 'utf8').split('\n')
|
|
113
|
+
let header = null
|
|
114
|
+
let restore = null
|
|
115
|
+
let lineNo = 0
|
|
116
|
+
let firstError = null
|
|
117
|
+
try {
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
if (!line.trim()) continue
|
|
120
|
+
lineNo++
|
|
121
|
+
if (header === null) {
|
|
122
|
+
header = JSON.parse(line)
|
|
123
|
+
// 与真实加载路径一致:带上该会话的子会话事实(v3→v4 迁移需要;无子会话=显式空数组)
|
|
124
|
+
const children = collectChildren(header.id)
|
|
125
|
+
const catalog = createSessionFormatCatalogWithChildren
|
|
126
|
+
? createSessionFormatCatalogWithChildren(children)
|
|
127
|
+
: sessionFormatCatalog
|
|
128
|
+
restore = catalog.createRestore(header, { recovery: 'strict', validation: 'transformed' })
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (firstError) continue
|
|
132
|
+
try {
|
|
133
|
+
restore.decodeRow(JSON.parse(line))
|
|
134
|
+
} catch (e) {
|
|
135
|
+
firstError = `行 ${lineNo}: ${e.message}`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (firstError) throw new Error(firstError)
|
|
139
|
+
const artifact = restore.finish()
|
|
140
|
+
console.log(`✅ ${file}`)
|
|
141
|
+
console.log(` v${header.version} → v${artifact.header.version}, 事件 ${artifact.events.length}, 行 ${lineNo}`)
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.log(`❌ ${file}`)
|
|
144
|
+
console.log(` ${e.message}`)
|
|
145
|
+
failed++
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
process.exit(failed > 0 ? 1 : 0)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TRANSPORT 诊断补丁校验(2026-10-02,任务 D)。
|
|
4
|
+
*
|
|
5
|
+
* 补丁:`dsh-llm-deepseek--transport-diagnostics.patch`
|
|
6
|
+
* 目标:`@deepseek-ai/dsh-llm-deepseek/lib/index.js`(官方 Messages 适配器;
|
|
7
|
+
* **问述代理复用同一份**,见 `plugins/argent-core/src/llm-whyshu.js`)。
|
|
8
|
+
*
|
|
9
|
+
* ## 为什么要有它
|
|
10
|
+
* 线上现象:Web 回合偶发 `This turn failed / DeepSeek Messages transport failed`。
|
|
11
|
+
* 实测(session-06732069 / session-878f3e3b 的会话日志)该 failure 落进 `llm/retry`
|
|
12
|
+
* 事件时**只有 `{message, code}`**,没有 cause、没有请求标识、没有"已产出多少"
|
|
13
|
+
* ⇒ 事后既分不清 ECONNRESET / 提前关闭 / 5xx 断流,也无法判断"这次到底产出了没有"。
|
|
14
|
+
* 补丁只做**可观测性**(判定语义不变)。
|
|
15
|
+
*
|
|
16
|
+
* ## 本脚本验什么
|
|
17
|
+
* ① 源码语义断言:三分支判定**未被改动**(顺序、code、`instanceof LlmError` 直通都还在);
|
|
18
|
+
* ② 行为断言:把补丁插入的纯函数**取出来真跑**,逐个覆盖
|
|
19
|
+
* ECONNRESET / socket hang up / ETIMEDOUT / 非 2xx + 响应体片段 / AbortError,
|
|
20
|
+
* 以及"已产出长度"的累加(文本/思维链/工具参数/块);
|
|
21
|
+
* ③ 装配断言:`registerDeepSeekProvider` 里确实把 `onTransportFailure` 接上了 logger;
|
|
22
|
+
* ④ 覆盖断言:`failure.requestId` 会被写进 LlmError(从而随 `llm/retry` 落进会话日志)。
|
|
23
|
+
*
|
|
24
|
+
* 用法:`node scripts/patches/verify-transport-diagnostics.mjs`(只读,不碰任何 session 文件)
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
27
|
+
import { requireDshScope } from '../lib/dsh-scope.mjs'
|
|
28
|
+
|
|
29
|
+
// 0.6.0 F1 修复批(2026-10-14):探不到 scope ⇒ 响亮失败(⛔ 不用假路径 —— 否则本脚本会
|
|
30
|
+
// 一路"目标不在 ⇒ 跳过",把"没验"报成"通过")。
|
|
31
|
+
let SCOPE
|
|
32
|
+
try { SCOPE = requireDshScope({ what: '补丁校验(verify-transport-diagnostics)' }) }
|
|
33
|
+
catch (e) { console.error(` ❌ ${e.message}`); process.exit(1) }
|
|
34
|
+
const TARGET = `${SCOPE}/dsh-llm-deepseek/lib/index.js`
|
|
35
|
+
const LLM = `${SCOPE}/dsh-llm/lib/index.js`
|
|
36
|
+
|
|
37
|
+
let pass = 0, fail = 0
|
|
38
|
+
const ok = (c, n) => { c ? (pass++, console.log(` ✅ ${n}`)) : (fail++, console.log(` ❌ ${n}`)) }
|
|
39
|
+
|
|
40
|
+
console.log('【TRANSPORT 诊断补丁】dsh-llm-deepseek/lib/index.js')
|
|
41
|
+
|
|
42
|
+
if (!existsSync(TARGET)) {
|
|
43
|
+
ok(false, `目标存在(${TARGET})`)
|
|
44
|
+
} else {
|
|
45
|
+
const src = readFileSync(TARGET, 'utf8')
|
|
46
|
+
|
|
47
|
+
// ── ① 判定语义不变(这是补丁的红线:只加可观测字段,不动三分支)────────────────
|
|
48
|
+
console.log('\n ── ① 判定语义未变 ──')
|
|
49
|
+
const catchBlock = (src.match(/\/\* Argent hotfix \(2026-10-02\):\*\*判定不变\*\*[\s\S]*?\n\t\t\t\} finally \{/) || [''])[0]
|
|
50
|
+
ok(/Argent hotfix \(2026-10-02\)/.test(src), '补丁标记存在')
|
|
51
|
+
const branches = [
|
|
52
|
+
/if \(timeoutOf\(watchdog\.signal, "MESSAGES_IDLE"\) !== void 0\) throw new LlmError\("DeepSeek Messages stream idle timeout", "TIMEOUT", \{ cause: error \}\);/,
|
|
53
|
+
/if \(options\.signal\?\.aborted\) throw new LlmError\("DeepSeek Messages request aborted", "ABORTED", \{ cause: error \}\);/,
|
|
54
|
+
/if \(error instanceof LlmError\) throw error;/,
|
|
55
|
+
]
|
|
56
|
+
ok(branches.every((re) => re.test(src)), 'TIMEOUT / ABORTED / LlmError-直通 三个分支**逐字未变**')
|
|
57
|
+
const order = [
|
|
58
|
+
src.indexOf('if (timeoutOf(watchdog.signal, "MESSAGES_IDLE") !== void 0) throw new LlmError("DeepSeek Messages stream idle timeout"'),
|
|
59
|
+
src.indexOf('if (options.signal?.aborted) throw new LlmError("DeepSeek Messages request aborted"'),
|
|
60
|
+
src.indexOf('if (error instanceof LlmError) throw error;'),
|
|
61
|
+
]
|
|
62
|
+
ok(order.every((i) => i > -1) && order[0] < order[1] && order[1] < order[2],
|
|
63
|
+
`三分支**顺序不变**(TIMEOUT → ABORTED → LlmError 直通;实测 ${JSON.stringify(order)})`)
|
|
64
|
+
ok(/throw new LlmError\("DeepSeek Messages transport failed", "TRANSPORT", \{/.test(src),
|
|
65
|
+
'TRANSPORT 仍是"其余一律"的兜底(message 与 code 逐字未变)')
|
|
66
|
+
ok(catchBlock !== '', 'catch 块里带"判定不变"注释(人改这里时会被提醒)')
|
|
67
|
+
|
|
68
|
+
// ── ② 行为断言:把补丁插入的纯函数取出来真跑 ────────────────────────────────
|
|
69
|
+
console.log('\n ── ② cause 形态与"已产出长度"(真跑补丁插入的函数) ──')
|
|
70
|
+
const start = src.indexOf('let argentTransportCallSeq = 0;')
|
|
71
|
+
const end = src.indexOf('/** DeepSeek provider using Messages content and native thinking replay. */', start)
|
|
72
|
+
const body = start === -1 || end === -1 ? '' : src.slice(start, end)
|
|
73
|
+
let H = null
|
|
74
|
+
try {
|
|
75
|
+
H = new Function(`${body}
|
|
76
|
+
const ProviderRequestId = (id) => id;
|
|
77
|
+
return { argentTransportDiagnostics, argentRecordProducedChunk, argentProducedSummary, argentRequestIdOf, argentNoteResponse, argentNoteHttpFailure, argentDescribeTransportCause, argentTransportReport, argentTransportLogLine };`)()
|
|
78
|
+
} catch (e) { ok(false, `补丁函数可独立求值(${e.message})`) }
|
|
79
|
+
ok(H !== null && typeof H.argentTransportReport === 'function',
|
|
80
|
+
'补丁插入的纯函数可独立求值(用它们做行为断言,不依赖完整 ctx)')
|
|
81
|
+
|
|
82
|
+
if (H !== null) {
|
|
83
|
+
const conn = { streamIdleTimeoutMs: 300000 }
|
|
84
|
+
const diagFor = (opts = { provider: 'whyshu', model: 'deepseek-flash' }) => H.argentTransportDiagnostics(opts, conn)
|
|
85
|
+
const resp = (status, headers = {}) => ({ status, headers: new Headers(headers) })
|
|
86
|
+
|
|
87
|
+
// ── cause 分类 ──
|
|
88
|
+
const cases = {
|
|
89
|
+
reset: Object.assign(new TypeError('fetch failed'), { cause: Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET', errno: -54, syscall: 'read' }) }),
|
|
90
|
+
hangup: Object.assign(new Error('socket hang up'), { code: 'ECONNRESET' }),
|
|
91
|
+
etimedout: Object.assign(new Error('connect ETIMEDOUT 1.2.3.4:443'), { code: 'ETIMEDOUT', syscall: 'connect', errno: -60 }),
|
|
92
|
+
earlyClose: Object.assign(new TypeError('terminated'), { cause: Object.assign(new Error('other side closed'), { code: 'UND_ERR_SOCKET' }) }),
|
|
93
|
+
aborted: Object.assign(new Error('This operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' }),
|
|
94
|
+
refused: Object.assign(new TypeError('fetch failed'), { cause: Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' }) }),
|
|
95
|
+
}
|
|
96
|
+
// `report.cause` = 描述 `error.cause ?? error` —— undici 常把真因藏在 cause 上,
|
|
97
|
+
// 所以断言一律走 report(这才是真正写进日志的那个形态)。
|
|
98
|
+
const kindOf = (e) => H.argentTransportReport(diagFor(), e, conn).cause.kind
|
|
99
|
+
const kinds = Object.fromEntries(Object.entries(cases).map(([k, e]) => [k, kindOf(e)]))
|
|
100
|
+
ok(kinds.reset === 'ECONNRESET', `ECONNRESET(undici 常见形态:TypeError fetch failed + cause.code)⇒ kind=${kinds.reset}`)
|
|
101
|
+
ok(kinds.hangup === 'ECONNRESET', `socket hang up ⇒ kind=${kinds.hangup}`)
|
|
102
|
+
ok(kinds.etimedout === 'ETIMEDOUT', `ETIMEDOUT ⇒ kind=${kinds.etimedout}`)
|
|
103
|
+
ok(kinds.earlyClose === 'EARLY_CLOSE', `提前关闭(terminated / other side closed / UND_ERR_SOCKET)⇒ kind=${kinds.earlyClose}`)
|
|
104
|
+
ok(H.argentTransportReport(diagFor(), cases.earlyClose, conn).causeChain[1]?.code === 'UND_ERR_SOCKET',
|
|
105
|
+
'内层原始 code(UND_ERR_SOCKET)仍逐字保留在 causeChain 里(kind 只是给人看的上位分类)')
|
|
106
|
+
ok(kinds.aborted === 'AbortError', `AbortError 可区分(不误判成网络错)⇒ kind=${kinds.aborted}`)
|
|
107
|
+
ok(kinds.refused === 'ECONNREFUSED', `ECONNREFUSED ⇒ kind=${kinds.refused}`)
|
|
108
|
+
// 单节点版只看自己:外层 `fetch failed` 没有 code 时归 FETCH_FAILED(链在 report.causeChain 里)
|
|
109
|
+
ok(H.argentDescribeTransportCause(cases.reset).kind === 'FETCH_FAILED',
|
|
110
|
+
'单节点描述不被内层 cause 污染(外层 fetch failed 归 FETCH_FAILED,真因在 causeChain 里)')
|
|
111
|
+
const chain = H.argentTransportReport(diagFor(), cases.reset, conn).causeChain
|
|
112
|
+
ok(chain.length === 2 && chain[0].kind === 'FETCH_FAILED' && chain[1].kind === 'ECONNRESET',
|
|
113
|
+
`cause 链被完整摊开(外层 fetch failed → 内层 ECONNRESET;实测 ${JSON.stringify(chain.map((c) => c.kind))})`)
|
|
114
|
+
ok(chain[1].errno === '-54' && chain[1].syscall === 'read', 'errno/syscall 被带上(定位连接层用)')
|
|
115
|
+
|
|
116
|
+
// ── 已产出长度 ──
|
|
117
|
+
const d0 = diagFor()
|
|
118
|
+
const r0 = H.argentTransportReport(d0, cases.hangup, conn)
|
|
119
|
+
ok(r0.produced.chunks === 0 && r0.produced.anyOutput === false,
|
|
120
|
+
'**尚无任何输出**的回合:chunks=0 且 anyOutput=false(这正是"可安全重试"的判据)')
|
|
121
|
+
|
|
122
|
+
const d1 = diagFor()
|
|
123
|
+
H.argentRecordProducedChunk(d1, { type: 'block-start', index: 0, blockType: 'text' })
|
|
124
|
+
H.argentRecordProducedChunk(d1, { type: 'text-delta', index: 0, text: '你好世界' })
|
|
125
|
+
H.argentRecordProducedChunk(d1, { type: 'reasoning-delta', index: 1, text: '想一想' })
|
|
126
|
+
H.argentRecordProducedChunk(d1, { type: 'tool-call-delta', index: 2, id: 'c1', name: 'flow_failures', argumentsDelta: '{"flow":' })
|
|
127
|
+
const r1 = H.argentTransportReport(d1, cases.earlyClose, conn)
|
|
128
|
+
ok(r1.produced.chunks === 4 && r1.produced.anyOutput === true,
|
|
129
|
+
`**已产出内容**的回合被识别出来(${JSON.stringify(r1.produced)})`)
|
|
130
|
+
ok(r1.produced.textChars === 4 && r1.produced.reasoningChars === 3 && r1.produced.toolCallChars === 8,
|
|
131
|
+
`分类计数正确:text=${r1.produced.textChars} reasoning=${r1.produced.reasoningChars} toolArgs=${r1.produced.toolCallChars}`)
|
|
132
|
+
ok(r1.produced.toolCallNames.length === 1 && r1.produced.toolCallNames[0] === 'flow_failures',
|
|
133
|
+
'工具名被记下(判断"是否已经开始产生副作用"用)')
|
|
134
|
+
H.argentRecordProducedChunk(d1, { type: 'finish', reason: { kind: 'stop' } })
|
|
135
|
+
ok(H.argentProducedSummary(d1).settled === true, 'finish/usage 会标记 settled')
|
|
136
|
+
|
|
137
|
+
// ── 请求标识(streamId)与 HTTP 状态 ──
|
|
138
|
+
const d2 = diagFor()
|
|
139
|
+
H.argentNoteResponse(d2, resp(200, { 'x-request-id': 'req-abc-123' }))
|
|
140
|
+
const r2 = H.argentTransportReport(d2, cases.earlyClose, conn)
|
|
141
|
+
ok(r2.requestId === 'req-abc-123' && r2.httpStatus === 200,
|
|
142
|
+
`响应头里的 provider 请求标识被抓到(requestId=${r2.requestId}, status=${r2.httpStatus})`)
|
|
143
|
+
const d3 = diagFor()
|
|
144
|
+
H.argentNoteResponse(d3, resp(200, { 'request-id': 'rid-1' }))
|
|
145
|
+
H.argentNoteResponse(d3, resp(200, { 'x-deepseek-request-id': 'rid-2' }))
|
|
146
|
+
ok(H.argentRequestIdOf(new Headers({ 'request-id': 'a' })) === 'a'
|
|
147
|
+
&& H.argentRequestIdOf(new Headers({ 'x-request-id': 'b' })) === 'b'
|
|
148
|
+
&& H.argentRequestIdOf(new Headers({})) === undefined,
|
|
149
|
+
'三个头按同一优先级取值(request-id → x-request-id → x-deepseek-request-id)')
|
|
150
|
+
|
|
151
|
+
// ── 非 2xx + 响应体片段 ──
|
|
152
|
+
const d4 = diagFor()
|
|
153
|
+
H.argentNoteHttpFailure(d4, resp(502, { 'x-request-id': 'gw-1' }), '<html><body>502 Bad Gateway</body></html>')
|
|
154
|
+
const r4 = H.argentTransportReport(d4, Object.assign(new Error('invalid response status'), { code: 'HPE_INVALID_STATUS' }), conn)
|
|
155
|
+
ok(r4.httpStatus === 502 && /502 Bad Gateway/.test(r4.bodySnippet),
|
|
156
|
+
`非 2xx 的状态码与响应体片段被记下(status=${r4.httpStatus}, snippet=${JSON.stringify(r4.bodySnippet)})`)
|
|
157
|
+
const longBody = 'x'.repeat(5000)
|
|
158
|
+
const d5 = diagFor()
|
|
159
|
+
H.argentNoteHttpFailure(d5, resp(500), longBody)
|
|
160
|
+
ok(H.argentTransportReport(d5, cases.hangup, conn).bodySnippet.length === 400,
|
|
161
|
+
'响应体片段被截断到 400 字符(不刷屏)')
|
|
162
|
+
|
|
163
|
+
// ── 日志行:可 grep + 关键字段齐全 ──
|
|
164
|
+
const line = H.argentTransportLogLine(r2)
|
|
165
|
+
ok(line.startsWith('[argent-transport] '), '日志行以固定前缀 `[argent-transport]` 开头(便于 grep)')
|
|
166
|
+
for (const field of ['route=whyshu/deepseek-flash', 'callId=', 'providerRequestId=req-abc-123', 'httpStatus=200', 'cause=EARLY_CLOSE', 'produced={chunks:', 'elapsedMs=', 'idleTimeoutMs=300000']) {
|
|
167
|
+
ok(line.includes(field), `日志行含字段 ${field}`)
|
|
168
|
+
}
|
|
169
|
+
ok(!line.includes('\n'), '单行输出(不会被日志系统按多行拆散)')
|
|
170
|
+
const line4 = H.argentTransportLogLine(r4)
|
|
171
|
+
ok(line4.includes('bodySnippet='), '非 2xx 时日志行附 bodySnippet')
|
|
172
|
+
const line0 = H.argentTransportLogLine(r0)
|
|
173
|
+
ok(line0.includes('providerRequestId=(none)') && line0.includes('httpStatus=(no response)'),
|
|
174
|
+
'连响应都没拿到时也明确标注 (none)/(no response),不假装有值')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── ③ 装配:onTransportFailure 接到 logger ────────────────────────────────
|
|
178
|
+
console.log('\n ── ③ 装配与落盘 ──')
|
|
179
|
+
ok(/onTransportFailure: \(report\) => \{/.test(src) && /ctx\.logger\.warn\(argentTransportLogLine\(report\)\)/.test(src),
|
|
180
|
+
'registerDeepSeekProvider 把 onTransportFailure 接到 ctx.logger.warn')
|
|
181
|
+
ok(/ctx\.logger\.warn\("llm-deepseek: TRANSPORT detail %o"/.test(src),
|
|
182
|
+
'同时打一份结构化 detail(%o,供日志详情/工具解析)')
|
|
183
|
+
ok(/this\.dependencies\.onTransportFailure\?\.\(transportReport\)/.test(src),
|
|
184
|
+
'适配器侧用可选链调用(第三方复用 registerDeepSeekProvider 时不会因缺回调而炸)')
|
|
185
|
+
ok(/\.\.\.transportReport\.requestId === void 0 \? \{\} : \{ requestId: transportReport\.requestId \}/.test(src),
|
|
186
|
+
'requestId 被写进 LlmError 的字段 ⇒ `failure.requestId` 随 `llm/retry` 事件落进会话日志')
|
|
187
|
+
ok(/const transportDiagnostics = argentTransportDiagnostics\(options, connection\);/.test(src)
|
|
188
|
+
&& /argentRecordProducedChunk\(transportDiagnostics, next\.value\);/.test(src),
|
|
189
|
+
'generate() 里每产出一片就记一次(`yield` 之前,故计数与消费端所见一致)')
|
|
190
|
+
|
|
191
|
+
// ── ④ LlmError 真的保留 requestId(跨包契约,非本补丁可改)──────────────────
|
|
192
|
+
if (existsSync(LLM)) {
|
|
193
|
+
const llmSrc = readFileSync(LLM, 'utf8')
|
|
194
|
+
ok(/\.\.\.options\?\.requestId === void 0 \? \{\} : \{ requestId: options\.requestId \}/.test(llmSrc),
|
|
195
|
+
'dsh-llm 的 LlmError 会把 requestId 保留进 .failure(本补丁依赖该既有契约,未改动它)')
|
|
196
|
+
// 但"落进会话日志"还差三段链路,逐一钉住(否则 requestId 会停在内存里):
|
|
197
|
+
// 适配器 throw ⇒ LlmRuntime 的 adapterFailureChunk 转成终态 finish 块
|
|
198
|
+
// ⇒ normalizeLlmFailure 取 error.failure 快照 ⇒ failureSnapshot 白名单里必须有 requestId
|
|
199
|
+
ok(/function adapterFailureChunk\(error, signal\)/.test(llmSrc)
|
|
200
|
+
&& /kind: "error",\s*\n\s*failure/.test(llmSrc),
|
|
201
|
+
'抛出的适配器错误被 LlmRuntime 转成终态 finish 块(这才是 `agent/request-error` 被触发的路径)')
|
|
202
|
+
ok(/function normalizeLlmFailure\(value\)/.test(llmSrc)
|
|
203
|
+
&& /const carried = ownFailureSnapshot\(error\);\s*\n\s*if \(carried !== void 0 && carried\.code === ownErrorCode\(error\)\) return carried;/.test(llmSrc),
|
|
204
|
+
'normalizeLlmFailure 直接复用 LlmError 自带的 .failure 快照(code 一致时)—— requestId 不会被丢掉')
|
|
205
|
+
ok(/const requestId = candidate\.requestId;/.test(llmSrc)
|
|
206
|
+
&& /\.\.\.requestId === void 0 \? \{\} : \{ requestId \}/.test(llmSrc),
|
|
207
|
+
'failureSnapshot 的白名单里有 requestId(也校验类型,非空字符串才放行)')
|
|
208
|
+
// 端到端(真造一个 LlmError,走同一段 normalize 逻辑):确认 requestId 真的留在 failure 里
|
|
209
|
+
const { LlmError } = await import(LLM)
|
|
210
|
+
const probe = new LlmError('DeepSeek Messages transport failed', 'TRANSPORT', {
|
|
211
|
+
cause: new Error('read ECONNRESET'),
|
|
212
|
+
requestId: 'req_probe_1',
|
|
213
|
+
})
|
|
214
|
+
ok(probe.failure.requestId === 'req_probe_1' && probe.failure.code === 'TRANSPORT' && probe.failure.message === 'DeepSeek Messages transport failed',
|
|
215
|
+
`端到端:TRANSPORT 的 LlmError 带出 {code, message, requestId}(实测 ${JSON.stringify(probe.failure)})`)
|
|
216
|
+
const noId = new LlmError('DeepSeek Messages transport failed', 'TRANSPORT', { cause: new Error('x') })
|
|
217
|
+
ok(noId.failure.requestId === undefined && Object.keys(noId.failure).sort().join(',') === 'code,message',
|
|
218
|
+
'没拿到请求标识时不会伪造:failure 仍是 {message, code}(与既有行为一致)')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`\n── 合计:通过 ${pass} / 失败 ${fail}`)
|
|
223
|
+
if (fail > 0) {
|
|
224
|
+
console.log('❌ TRANSPORT 诊断补丁校验未通过:补丁可能没打上或上游已改动 —— 先别重启,见 docs / AGENTS.md')
|
|
225
|
+
process.exit(1)
|
|
226
|
+
}
|
|
227
|
+
console.log('✅ TRANSPORT 诊断补丁校验通过:cause / streamId / 已产出长度均可观测,判定语义未变')
|