nexforge-cli 0.4.0__tar.gz → 0.4.1__tar.gz
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.
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/PKG-INFO +8 -7
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/README.md +8 -7
- nexforge_cli-0.4.1/docs/decisions/2026-09-12-/345/206/205/345/256/271/351/207/214/347/232/204/346/240/207/350/256/260/344/270/215/345/276/227/345/275/223/346/210/220/346/266/210/346/201/257/350/276/271/347/225/214.md +65 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/README.md +1 -0
- nexforge_cli-0.4.1/docs/evidence/release-0.4.0.log +46 -0
- nexforge_cli-0.4.1/docs/evidence/session-marker-boundary.log +101 -0
- nexforge_cli-0.4.1/docs/evidence/session-marker-boundary_fulltest.log +4 -0
- nexforge_cli-0.4.1/docs/evidence/session-marker-boundary_live.log +84 -0
- nexforge_cli-0.4.1/docs/evidence/session-marker-boundary_report.md +77 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/01-/345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md +1 -1
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/README.md +1 -1
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/pyproject.toml +1 -1
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/__init__.py +1 -1
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/agent.py +16 -11
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/session.py +67 -48
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/store/session_store.py +39 -3
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/screens/chat.py +4 -3
- nexforge_cli-0.4.1/tests/test_session_repair.py +301 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_session_store.py +56 -0
- nexforge_cli-0.4.0/tests/test_session_repair.py +0 -141
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/.gitignore +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/INSTALL.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/STATS_API.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/config.example.toml +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/doc//344/273/243/347/240/201/344/274/230/345/214/226/350/256/241/345/210/222.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/doc//347/211/271/346/200/247/346/250/241/345/235/227/346/263/250/345/206/214/345/210/266-/345/256/236/346/226/275/350/256/241/345/210/222.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/doc//351/241/271/347/233/256/346/200/273/347/233/221/345/276/252/347/216/257-/345/274/200/345/217/221/350/256/241/345/210/222.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-10-fails-loud/345/210/206/347/272/247.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-10-/344/274/232/350/257/235/344/270/215/345/217/230/351/207/217.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-10-/344/276/235/350/265/226/344/270/200/350/207/264/346/200/247.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-10-/345/233/236/346/224/276/350/214/203/345/233/264/351/207/215/345/256/232/344/271/211.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-10-/345/256/241/350/256/241/344/273/245/350/260/203/347/224/250/351/223/276/344/270/272/345/207/206.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-10-/350/203/275/345/212/233/345/217/257/347/224/250/346/200/247/344/270/212/346/212/245.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-10-/350/264/250/351/207/217gate/344/275/223/347/263/273.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-11-deepseek/346/241/243/344/275/215/347/256/200/345/214/226.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-11-/346/241/243/344/275/215/347/256/200/345/214/226/344/270/272/345/215/225/346/241/243flash.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-11-/347/273/210/347/253/257/347/212/266/346/200/201/345/205/234/345/272/225/346/201/242/345/244/215.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-11-/351/205/215/347/275/256/346/256/213/347/225/231/345/200/274/346/262/273/347/220/206.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/decisions/2026-09-11-/351/207/215/350/257/225/345/275/222/344/270/200/345/261/202.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D10_acceptance.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D10_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D11_a4.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D11_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D12_audit.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D12_gap.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D13_break.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D13_e2e.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D13_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D13_gaps.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D14_audit.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D14_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D14_recheck.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D15_audit.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D4_fast.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D4_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D4_hook.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D4_scene_c.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D4_scene_d.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D4_scene_e.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D5_audit.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D5_b2_plugin.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D5_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D6_b3_a.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D6_b3_b.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D6_b3_c.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D6_b4_provider.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D6_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D7_dedup.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D7_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D8_audit.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D8_invariant.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D9_after.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D9_before.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D9_decision.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D9_full_test.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/D9_rebuild.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_cap_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_capabilities.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_engines.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_engines_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_tts_availability.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_tts_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_tts_installer.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_vision_channels.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/E_vision_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/F_decisions_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/deps_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/deps_reverse.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/exit-cost.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/final_check_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/final_check_pypi.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/final_check_report.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/fix_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/health_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/health_report.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/health_scan.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/model-sidebar-dot.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/model_rename_check.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/provider-family-check.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/release-0.3.0.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/release_0218_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/release_0218_pypi.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/simplify_tier_fulltest.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/evidence/terminal-leak-conditions.log +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/session-invariant.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/submission/README-/346/212/225/347/250/277/350/257/264/346/230/216.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/submission/nexforge.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/docs/submission/nexforge.zh-CN.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/02-/347/225/214/351/235/242/344/270/216/345/277/253/346/215/267/351/224/256.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/03-/346/250/241/345/236/213/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/04-/345/267/245/345/205/267/345/217/202/350/200/203.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/05-/346/225/260/346/215/256/344/270/216/346/220/234/347/264/242.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/06-/346/235/203/351/231/220/344/270/216/346/250/241/345/274/217.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/07-/344/274/232/350/257/235/344/270/216/350/256/260/345/277/206.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/08-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/09-/345/270/270/350/247/201/351/227/256/351/242/230.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/10-/346/217/222/344/273/266/345/274/200/345/217/221.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual/11-/345/274/200/345/217/221/346/240/241/351/252/214.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/manual//351/231/204/345/275/225-/345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/README.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/guiauto/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/guiauto/action.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/guiauto/base.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/guiauto/tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/guiauto/vision.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/guiauto/window.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/install_plugin.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/requirements.txt +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/smoke_safe.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/verify_final.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/verify_host.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_gui_auto/verify_plugin.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_speech_summary/README.md +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_speech_summary/install_plugin.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/plugin_speech_summary/speech_summary.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/check.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/exit_cost_probe.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/exit_smoke.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/base.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/eof_newline.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/example_config.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/hygiene.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/manual_consistency.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/registry_consistency.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/gates/version_sync.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/install_hooks.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/publish.sh +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/app.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/config.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/capability.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/context.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/federation.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/goal_manager.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/knowledge_base.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/memory_engine.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/model_catalog.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/plugin.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/project_mapper.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/provider.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/summarizer.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/system_inject.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/tool_manifest.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/core/update_check.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/auto_skills.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/base.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/federation_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/kb_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/map_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/memory_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/mode_features.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/plan_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/registry.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/session_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/slash_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/stats_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/features/tool_manifest_feature.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/i18n.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/provider/_retry.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/provider/anthropic.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/provider/deepseek.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/provider/openai.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/provider/qianwen.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/stats/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/stats/heartbeat.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/store/director_store.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/terminal_guard.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/theme.tcss +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/data_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/federation.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/image_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/kb_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/ops_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/registry.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/screenshot_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/search.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/clipboard_image.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/screen_api.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/screens/chat_actions.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/screens/splash.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/slash_handler.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/file_browser.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/file_menu.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/image_sidebar.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/messages.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/mode_warning.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/model_sidebar.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/ui/widgets/statusbar.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/utils/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/utils/logging.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/web/__init__.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/conftest.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_app_mouse.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_approval.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_archive_integrity.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_capability.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_check_gates.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_config.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_cross_platform.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_deepseek_provider.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_deps.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_director_loop.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_director_store.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_dual_panel.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_exit_smoke.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_federation.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_file_browser.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_goal_confirm.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_i18n.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_icons.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_lang_cmd.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_manual_consistency.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_model_catalog.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_model_sidebar_dot.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_p2_core.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_plugin.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_plugin_example_tts.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_project_map_prune.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_provider_errors.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_quit_paths.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_retry_policy.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_sanitize.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_screenshot_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_session_invariant.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_session_save_safety.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_sidebar_align.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_splash_modal.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_statusbar.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_system_inject.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_terminal_guard.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_tool_iters.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_tools.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_tui.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_update_check.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_vision_capability.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_vision_model.py +0 -0
- {nexforge_cli-0.4.0 → nexforge_cli-0.4.1}/tests/test_vision_tool_retention.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nexforge-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)
|
|
5
5
|
Project-URL: Homepage, https://gitee.com/Lighthorn/nexforgecli
|
|
6
6
|
Author: NexForge
|
|
@@ -51,13 +51,14 @@ Description-Content-Type: text/markdown
|
|
|
51
51
|
|
|
52
52
|
NexForgeCLI 是一个运行在终端里的智能体助手,既支持 DeepSeek/千问等多模型,也是**插件宿主**(plugins 目录即插即用)。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
|
|
53
53
|
|
|
54
|
-
> 当前版本:**v0.4.
|
|
54
|
+
> 当前版本:**v0.4.1** · PyPI: `pip install nexforge-cli`
|
|
55
55
|
|
|
56
|
-
**v0.4.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
**v0.4.1 要点(重要修复)**:会话重建时,**内容里出现的 `<!-- nf-msg … -->` 不再被当成消息边界**。
|
|
57
|
+
此前若工具结果 dump 过会话文件(或粘贴过会话内容),加载时那些标记会被解码成额外消息、
|
|
58
|
+
插进工具块中间,导致**每轮请求 400、项目再也打不开**。
|
|
59
|
+
修法:读侧标记识别改为**行首锚定**,写侧对内容里的标记前缀做转义;同时把工具序列修复
|
|
60
|
+
从"只修尾部"升级为"修全序列"(中段违规也能纠正)。
|
|
61
|
+
另含 0.4.0 推出的:本机 TTS 优先/回退、模型随 nf 常驻、`/voice full|brief`、配置残留值自动收敛。
|
|
61
62
|
|
|
62
63
|
---
|
|
63
64
|
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
NexForgeCLI 是一个运行在终端里的智能体助手,既支持 DeepSeek/千问等多模型,也是**插件宿主**(plugins 目录即插即用)。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
|
|
10
10
|
|
|
11
|
-
> 当前版本:**v0.4.
|
|
12
|
-
|
|
13
|
-
**v0.4.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
> 当前版本:**v0.4.1** · PyPI: `pip install nexforge-cli`
|
|
12
|
+
|
|
13
|
+
**v0.4.1 要点(重要修复)**:会话重建时,**内容里出现的 `<!-- nf-msg … -->` 不再被当成消息边界**。
|
|
14
|
+
此前若工具结果 dump 过会话文件(或粘贴过会话内容),加载时那些标记会被解码成额外消息、
|
|
15
|
+
插进工具块中间,导致**每轮请求 400、项目再也打不开**。
|
|
16
|
+
修法:读侧标记识别改为**行首锚定**,写侧对内容里的标记前缀做转义;同时把工具序列修复
|
|
17
|
+
从"只修尾部"升级为"修全序列"(中段违规也能纠正)。
|
|
18
|
+
另含 0.4.0 推出的:本机 TTS 优先/回退、模型随 nf 常驻、`/voice full|brief`、配置残留值自动收敛。
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 决策:内容里的 `<!-- nf-msg … -->` 不得被当成消息边界
|
|
2
|
+
|
|
3
|
+
> 日期:2026-09-12 · 状态:生效
|
|
4
|
+
|
|
5
|
+
## 背景
|
|
6
|
+
|
|
7
|
+
会话重建的权威是每行消息末尾的结构化标记 `<!-- nf-msg <base64> -->`(`docs/session-invariant.md` §4.1)。
|
|
8
|
+
但解析用的是**全文 `findall`**,于是"内容里出现的标记"与"真正的消息边界"无法区分。
|
|
9
|
+
|
|
10
|
+
触发链(实测,NexForgeCLI 自己的 `session-122357`):
|
|
11
|
+
|
|
12
|
+
1. 工具结果 dump 了某个会话 `.md` 文件(`read_file .nexforge/sessions/session-…md`),
|
|
13
|
+
被 dump 的标记行作为**内容**落进新会话文件;
|
|
14
|
+
2. 下次加载时,这些"内容里的标记"被解码成**额外消息**,且插在
|
|
15
|
+
`assistant(tool_calls)` 与它的 `tool` 响应之间(实测多出 10 份重复助手消息);
|
|
16
|
+
3. 请求违反 OpenAI/DeepSeek 规则——"带 tool_calls 的 assistant 后必须紧跟覆盖全部
|
|
17
|
+
tool_call_id 的 tool 消息"——返回 400
|
|
18
|
+
`insufficient tool messages following tool_calls message`;
|
|
19
|
+
4. `_repair_incomplete_tail` 只修**尾部**,看不见中段违规 → 每轮请求都 400。
|
|
20
|
+
用户侧现象:**"项目再也打不开"**(打开后说什么都报错)。
|
|
21
|
+
|
|
22
|
+
## 决策
|
|
23
|
+
|
|
24
|
+
三层一起上(缺任一层都有残留口子):
|
|
25
|
+
|
|
26
|
+
1. **写侧转义**(`_escape_marker_prefix`):人读部分里出现的 `<!-- nf-msg` 一律插入零宽空格
|
|
27
|
+
`\u200b` 转义。标记内的 base64 负载不受影响 → 重建权威无损
|
|
28
|
+
(§4.1 本就允许人读部分变形/截断)。
|
|
29
|
+
2. **读侧锚定**(`_MARKER_RE` 加 `re.M` 与行首 `^`):只有**独立成行**的标记才是边界。
|
|
30
|
+
真标记必然在行首(所有人类可读块都以换行结尾),全仓库历史会话文件实测零反例。
|
|
31
|
+
3. **全序列修复**(`_repair_incomplete_tail` → `_repair_tool_sequence`,旧名保留为别名):
|
|
32
|
+
修复**整条序列**而不是尾部 —— 夹层消息**保序后移**、缺响应的 tool_call 补"已取消"、
|
|
33
|
+
重复响应与无主孤儿 tool 删除。修复后的序列对 API **恒合法**。
|
|
34
|
+
配套:`agent.py` 的防死循环提示改为在整块工具响应**之后**追加,不再插进 tool 之间。
|
|
35
|
+
|
|
36
|
+
## 理由
|
|
37
|
+
|
|
38
|
+
- **只修尾部不够**:违规可能在中段(本次实测),尾部修复完全看不到。
|
|
39
|
+
- **只加读侧锚定不够**:不带行号前缀的内容(用户把会话文件整段粘进对话、shell 直接
|
|
40
|
+
`type` 出文件)仍能伪造出行首标记。
|
|
41
|
+
- **只加写侧转义不够**:修复前落盘的历史文件里已有的镶嵌标记仍会被解码 → 需要读侧 +
|
|
42
|
+
全序列修复兜底(已用真机 `session-122357` 验证:修复后非法点 0)。
|
|
43
|
+
- **否决"按人读格式严格比对,再判定某个标记是否可信"**:人读格式一旦演进,旧文件会被
|
|
44
|
+
整段判成伪标记 → **静默丢历史**,代价远高于收益(不变量只禁止一件事:无声地丢失)。
|
|
45
|
+
|
|
46
|
+
## 代价与边界
|
|
47
|
+
|
|
48
|
+
- 人读部分与原始内容有肉眼不可见的差异(零宽空格);工具结果原文仍由标记负载精确重建。
|
|
49
|
+
- 夹层消息只"后移"不删除:已积累的重复消息仍占上下文(必要时人工清理)。
|
|
50
|
+
- **复用注意**:`_repair_tool_sequence` 为兼容旧行为会弹出**尾部未配对 user**;因此它之后必须
|
|
51
|
+
紧跟 `add_user`(app 的 `run_turn` 顺序)。若单独调用后直接发请求,列表会以 assistant 结尾,
|
|
52
|
+
thinking 模式下会触发另一条 400 `reasoning_content ... must be passed back`
|
|
53
|
+
(见 `docs/evidence/session-marker-boundary_report.md` §5 坑 1)。
|
|
54
|
+
- **已知未覆盖(本次不改,另行决策)**:`save_session` 不清理"陈旧分段"(列表变短时,
|
|
55
|
+
旧的高编号分段文件仍会被 `load_session` 追加 → 陈旧内容混入 + 可能非法序列)。
|
|
56
|
+
机制已在证据脚本里复现(3 条被加载成 130 条);全序列修复能兜住 400,但内容冗余仍在。
|
|
57
|
+
|
|
58
|
+
## 关联
|
|
59
|
+
|
|
60
|
+
- 模块:`store/session_store.py`(`_MARKER_RE` / `_escape_marker_prefix` / `_format_message`)、
|
|
61
|
+
`core/session.py`(`_repair_tool_sequence`)、`core/agent.py`(提示位置)
|
|
62
|
+
- 文档:`docs/session-invariant.md` §4.1
|
|
63
|
+
- 测试:`tests/test_session_store.py`(往返)、`tests/test_session_repair.py`(全序列)、
|
|
64
|
+
`tests/test_session_invariant.py`
|
|
65
|
+
- 证据:`docs/evidence/session-marker-boundary_report.md` / `.log`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 发布 0.4.0 记录(2026-09-11)
|
|
2
|
+
|
|
3
|
+
## 内容(相对 0.3.0)
|
|
4
|
+
|
|
5
|
+
0.3.0 之后共 **10 个提交 / 23 文件(+1827 / −125)**:
|
|
6
|
+
|
|
7
|
+
- **feat** 语音播报接入**本机 TTS**(如本地部署的 CosyVoice):本机优先、失败自动回退
|
|
8
|
+
微软 Windows 语音(`122481f`);
|
|
9
|
+
- **feat** 用 nf 时让本机模型**常驻显存**、nf 全部退出即释放 —— 租约机制 + PID 存活/
|
|
10
|
+
心跳超时自愈,被强杀也能归还显存(`c4f033a`、`67b33bc`);
|
|
11
|
+
- **feat** 新增 `/voice full|brief` 切换播报范围,默认**全量播报**最终报告
|
|
12
|
+
(`max_chars` 默认 100 → 0)(`0d62b3d`);
|
|
13
|
+
- **fix** 配置残留值治理:失效 provider 自动回退内置 deepseek(不再让 TUI 初始化失败、
|
|
14
|
+
不再连带报误导性的"缺少 API Key");`[provider] verified` 不再被当成未知字段误报
|
|
15
|
+
(`3f95873`、`660e27e`);
|
|
16
|
+
- **test** 新增两条门禁:`eof-newline`(防"黏行"损坏)、`example-config`
|
|
17
|
+
(示例配置必须等于代码默认值 —— 它当场抓出两处漂移,其中 `max_output_tokens=8192`
|
|
18
|
+
会误导用户复现"正文截断")(`84a7f5f`、`efad92c`);
|
|
19
|
+
- **docs** ADR《配置残留值治理》+ 插件/服务文档修正(`4001050`、`d99e3e5`)。
|
|
20
|
+
|
|
21
|
+
## 发布流程与结果
|
|
22
|
+
|
|
23
|
+
| 步骤 | 结果 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| 版本 | 0.3.0 → **0.4.0**(5 处同步: pyproject / `__version__` / README / 手册索引 / 手册第 1 章) |
|
|
26
|
+
| 全量测试 | **567 passed, 3 skipped** |
|
|
27
|
+
| 质量 gate | **6/6 PASS**(含 version-sync、example-config、eof-newline) |
|
|
28
|
+
| 构建 | whl 292 KB + tar.gz 461 KB |
|
|
29
|
+
| 产物自检 | `twine check` → **两个包 PASSED** |
|
|
30
|
+
| 上传前本地安装 | rc=0;新装环境版本 0.4.0、`tts.mode/resident/max_chars` 齐全、`/voice` 在包内 |
|
|
31
|
+
| 上传 | `twine upload`(带 `PYTHONIOENCODING=utf-8`)→ 成功 |
|
|
32
|
+
| PyPI | https://pypi.org/project/nexforge-cli/0.4.0/ |
|
|
33
|
+
|
|
34
|
+
## 又一次踩到"传播延迟",且这次是**两个不同的缓存**
|
|
35
|
+
|
|
36
|
+
1. 上传后立刻查**顶层 JSON API** → 先显示 `最新版本: 0.3.0`,**约 40 秒后**变为 0.4.0、
|
|
37
|
+
文件数 2 ✔;
|
|
38
|
+
2. 但 **pip 用的 `/simple` 索引是另一个缓存**:JSON 已经更新时,`pip install
|
|
39
|
+
nexforge-cli==0.4.0` 仍报 `No matching distribution found (available: … 0.3.0)`,
|
|
40
|
+
**约 20 秒后**才可见。
|
|
41
|
+
|
|
42
|
+
**差点产生的假绿**:pip 失败后,我的校验脚本 `import nexforge` 回退到了**本地仓库包**,
|
|
43
|
+
却照样打印出 `0.4.0`(因为仓库里就是 0.4.0)—— 是"**打印导入路径**"这一步把它揭穿的
|
|
44
|
+
(显示 `src` 而不是安装目录)。**教训**:发布后验证必须
|
|
45
|
+
(1) 分别等待 JSON 与 `/simple` 两个缓存的传播,
|
|
46
|
+
(2) 打印导入路径确认来源。
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 2026-09-12 会话标记边界修复 — 原始证据(诊断脚本输出, 未编辑)
|
|
2
|
+
|
|
3
|
+
复现命令(诊断脚本为临时脚本, 逻辑就是"用项目自己的 load_session 加载 + 按 OpenAI/DeepSeek
|
|
4
|
+
规则校验工具序列"):
|
|
5
|
+
|
|
6
|
+
python verify_real.py D:\DevelopProject\NexForge\NexForgeCLI session-122357
|
|
7
|
+
python before_after.py # 旧行为 vs 修复后
|
|
8
|
+
python stale_seg_sim.py # 遗留风险(陈旧分段)模拟
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [1] 内容里的标记 → 幽灵消息: 旧行为 vs 修复后(`before_after.py`)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
【修复后】消息数 = 3 期望 = 3
|
|
16
|
+
【修复后】序列: [('user', None), ('assistant', None), ('tool', 'c1')]
|
|
17
|
+
|
|
18
|
+
【旧行为】消息数 = 4
|
|
19
|
+
【旧行为】序列: [('user', None), ('assistant', None), ('assistant', None), ('tool', 'c1')]
|
|
20
|
+
|
|
21
|
+
修复后 API 合法: True
|
|
22
|
+
旧行为 API 合法: False ← False 即 DeepSeek 400
|
|
23
|
+
结论: 旧行为凭空多出消息且序列非法(400); 修复后逐项一致。
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
要点: 工具结果内容里嵌一行真标记形状, 旧行为会把它解码成**一条幽灵 assistant 消息**,
|
|
27
|
+
且插在 `assistant(tool_calls)` 与 `tool` 响应之间 → 序列非法(400)。
|
|
28
|
+
|
|
29
|
+
## [2] 真机会话 `session-122357`: 加载 → 全序列修复 → 校验(`verify_real.py`)
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
=== D:\DevelopProject\NexForge\NexForgeCLI :: session-122357 ===
|
|
33
|
+
加载消息数: 3785 角色分布: {'system': 1, 'user': 44, 'assistant': 2673, 'tool': 1067}
|
|
34
|
+
idx=1724 第2个 tool_call 后不是其响应 → 实际下一条 role=assistant
|
|
35
|
+
idx=1724 第3个 tool_call 后不是其响应 → 实际下一条 role=assistant
|
|
36
|
+
idx=1729 孤儿 tool(无前置 assistant) → 实际下一条 role=call_01_ntfoM5j9cu4Ee5Yx0Tyq5025
|
|
37
|
+
idx=1730 孤儿 tool(无前置 assistant) → 实际下一条 role=call_02_UWDVr1XVkUCnkmPX0HcO6758
|
|
38
|
+
修复后消息数: 3785 (净变化 +0) 非法点: 0
|
|
39
|
+
非 tool 消息丢失数: 0 (必须为 0)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
说明:
|
|
43
|
+
|
|
44
|
+
- 违规点在 **idx=1724(全长 3785 的中段)** —— 这正是旧 `_repair_incomplete_tail`(只修尾部)
|
|
45
|
+
兜不住、导致"每轮都 400"的原因;
|
|
46
|
+
- 修复后非法点 0, 且**非 tool 消息一条不丢**(夹层消息只后移, 不删除);
|
|
47
|
+
- 修复后加载条数比修复前少 1(3786 → 3785): 少的正是"内容里的标记"伪造出的幽灵消息,
|
|
48
|
+
被读侧行首锚定挡住。
|
|
49
|
+
|
|
50
|
+
## [3] 遗留风险模拟: 陈旧分段残留(`stale_seg_sim.py`, 本次未修, 已记录)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
第一次保存(60 轮)分段文件: 3
|
|
54
|
+
第二次保存(3 条)后分段文件: 3 (旧文件仍在 → 陈旧残留)
|
|
55
|
+
|
|
56
|
+
加载结果: 130 条(期望 3)
|
|
57
|
+
加载后非法点: 0
|
|
58
|
+
全序列修复后消息数: 130 非法点: 0
|
|
59
|
+
→ 修复兜住了 400, 但陈旧内容仍混入上下文(冗余/重复)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
说明: `save_session` 写完新分段后不清理"超出新段数的旧分段文件", 而 `load_session`
|
|
63
|
+
按序号**连续**读 → 列表变短(上下文压缩等)后, 旧分段会被追加回来。
|
|
64
|
+
本次修复能让请求保持合法(不再永久 400), 但内容冗余仍在; 建议后续用"文件头声明分段数"
|
|
65
|
+
收口(见 ADR `docs/decisions/2026-09-12-内容里的标记不得当成消息边界.md` 的"代价与边界")。
|
|
66
|
+
|
|
67
|
+
## [4] 全量测试与本机巡检
|
|
68
|
+
|
|
69
|
+
- 全量 `pytest -q`: 见 `session-marker-boundary_fulltest.log`(**577 passed / 3 skipped**);
|
|
70
|
+
注: 直接把 pytest 输出重定向到文件时, `sys.__stderr__` 走 GBK 会把中文断言串编坏,
|
|
71
|
+
`test_session_save_safety` / `test_terminal_guard` 两条会假 FAIL —— 加
|
|
72
|
+
`PYTHONIOENCODING=utf-8` 即全绿(与本次改动无关, 供后来者参考);
|
|
73
|
+
- 本机所有会话巡检(只读): 仅 `NexForgeCLI/.nexforge/sessions/session-122357.md` 命中非法序列,
|
|
74
|
+
修复后 0; `CosyVoice` 两个会话本就合法; 未发现真实存在的陈旧分段文件。
|
|
75
|
+
|
|
76
|
+
## [5] 真机 API 验证(唯一联网步骤; 目标是用户自己配置的 deepseek)
|
|
77
|
+
|
|
78
|
+
顺序复刻 app 的 `run_turn`: **修复 → add_user → 发请求**。原始输出见
|
|
79
|
+
`session-marker-boundary_live.log`, 摘要:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
--- 合成最小复现 ---
|
|
83
|
+
[400 ] 修复前(只修尾部 → 中段违规保留): 400 "must be followed by tool messages responding ..."
|
|
84
|
+
[OK ] 修复后(全序列修复): 回复='我没法执行 git 命令——我目前只有 read_file ...'
|
|
85
|
+
|
|
86
|
+
--- 用户真机会话切片(idx 1715..1735, 含 10 份重复消息) ---
|
|
87
|
+
[400 ] 修复前: 400 "must be followed by tool messages responding ..."(与截图同款)
|
|
88
|
+
[OK ] 修复后: 回复='先做两件事并举:把截图切块放大精读…'
|
|
89
|
+
|
|
90
|
+
--- 用户会话全部 3785 条端到端 ---
|
|
91
|
+
[OK ] 加载 3785 条; content 字符 896103 → 回复='好'
|
|
92
|
+
usage: prompt=980288 completion=2
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
附带发现(见报告 §5): 若自行构造的请求**以 assistant 结尾**(脱离 `run_turn` 的
|
|
96
|
+
"修复 → add_user"顺序), thinking 模式会报另一条 400
|
|
97
|
+
`The reasoning_content in the thinking mode must be passed back to the API.`
|
|
98
|
+
app 的真实路径永远以 user 结尾, 不受影响; 排查时先看请求最后一条的 role。
|
|
99
|
+
|
|
100
|
+
体量标定: 该会话 ≈ **980k prompt tokens**(按真机 usage 实测) —— 已逼近模型窗口,
|
|
101
|
+
建议该项目后续工作改开新会话(详见报告 §6)。
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
..........................................................................................................................sss........................................................................................................... [ 40%]
|
|
2
|
+
........................................................................................................................................................................................................................................ [ 80%]
|
|
3
|
+
.................................................................................................................... [100%]
|
|
4
|
+
577 passed, 3 skipped in 166.00s (0:02:46)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# 2026-09-12 真机 API 验证 — 原始输出
|
|
2
|
+
# 说明: 唯一联网的部分。调用的是用户自己配置的 DeepSeek(极少量 token)。
|
|
3
|
+
# 顺序复刻 app: run_turn = 修复(全序列) → add_user → 发请求。
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
======================================================================
|
|
7
|
+
[1] 合成最小复现 + 用户真机切片(修复前 400 / 修复后 OK)
|
|
8
|
+
======================================================================
|
|
9
|
+
|
|
10
|
+
--- 合成最小复现 (合成, 6 条) ---
|
|
11
|
+
[400 ] 合成最小复现: 修复前(只修尾部 → 中段违规保留)
|
|
12
|
+
尾部形态=[('system', ''), ('user', ''), ('assistant', ''), ('tool', 'call_a'), ('assistant', ''), ('tool', 'call_b'), ('user', '')]
|
|
13
|
+
DeepSeek 请求失败:Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insuff
|
|
14
|
+
[OK ] 合成最小复现: 修复后(全序列修复)
|
|
15
|
+
尾部形态=[('system', ''), ('user', ''), ('assistant', ''), ('tool', 'call_a'), ('tool', 'call_b'), ('assistant', ''), ('user', '')]
|
|
16
|
+
回复='我没法执行 git 命令——我目前只有 `read_file'
|
|
17
|
+
|
|
18
|
+
--- 用户真机切片 (真机会话 idx 1715..1735, 20 条) ---
|
|
19
|
+
[400 ] 用户真机切片: 修复前(只修尾部 → 中段违规保留)
|
|
20
|
+
尾部形态=[('assistant', ''), ('assistant', ''), ('assistant', ''), ('assistant', ''), ('tool', 'call_00_KUQ0QE4uHYFZR9vECKHs8585'), ('tool', 'call_01_vPwNaGPVtYL6pjDSz1Jp1970'), ('assistant', ''), ('user', '')]
|
|
21
|
+
DeepSeek 请求失败:Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insuff
|
|
22
|
+
[OK ] 用户真机切片: 修复后(全序列修复)
|
|
23
|
+
尾部形态=[('assistant', ''), ('assistant', ''), ('tool', 'call_00_KUQ0QE4uHYFZR9vECKHs8585'), ('tool', 'call_01_vPwNaGPVtYL6pjDSz1Jp1970'), ('assistant', ''), ('tool', 'call_00_lGLB89578wxkRZEHAc7h3860'), ('tool', 'call_01_PSu2XcMwXvYpfFx5HyeH0236'), ('user', '')]
|
|
24
|
+
回复='先做两件事并举:把截图切块放大精读(上次裁剪被中断了),同时'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
======================================================================
|
|
29
|
+
[2] 用户会话**全部 3785 条**端到端(修复后 → OK, 980k prompt tokens)
|
|
30
|
+
======================================================================
|
|
31
|
+
加载 3785 条; content 字符 896103
|
|
32
|
+
OK 回复='好'
|
|
33
|
+
usage: prompt=980288 completion=2 cache_hit=0 cache_miss=980288
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
======================================================================
|
|
38
|
+
[3] token 折算标定(用于估算会话体量)
|
|
39
|
+
======================================================================
|
|
40
|
+
样本: 10 条, content 字符 2677, 实测 prompt_tokens 1809
|
|
41
|
+
折算比 ≈ 1.48 字符/token(含消息包装开销, 偏保守)
|
|
42
|
+
全会话 content 字符 896103 → 估算 ≈ 605,547 tokens
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
======================================================================
|
|
47
|
+
[4] 排查过程中的一个反例(见报告 §5 解释: 请求以 assistant 结尾会触发另一条 400)
|
|
48
|
+
======================================================================
|
|
49
|
+
A(直接构造) 哈希: 032cb06ac43097bb [('system', '', False, False), ('user', '', False, False), ('assistant', '', True, True), ('tool', 'call_a', False, False), ('tool', 'call_b', False, False), ('assistant', '', False, False), ('user', '', False, False)]
|
|
50
|
+
B(违规→修复) 哈希: 155c87cb5fdfbe12 [('system', '', False, False), ('user', '', False, False), ('assistant', '', True, True), ('tool', 'call_a', False, False), ('tool', 'call_b', False, False), ('assistant', '', False, False)]
|
|
51
|
+
两者规范形态一致: False
|
|
52
|
+
A#1 OK: '我没法直接运行 git 命令'
|
|
53
|
+
A#2 OK: '我没法真正“检查 git 提'
|
|
54
|
+
A#3 OK: '好'
|
|
55
|
+
B#1 400(reasoning): DeepSeek 请求失败:Error code: 400 - {'error': {'message': 'The `reasoning_content` in the thinking mode
|
|
56
|
+
B#2 400(reasoning): DeepSeek 请求失败:Error code: 400 - {'error': {'message': 'The `reasoning_content` in the thinking mode
|
|
57
|
+
B#3 400(reasoning): DeepSeek 请求失败:Error code: 400 - {'error': {'message': 'The `reasoning_content` in the thinking mode
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
======================================================================
|
|
62
|
+
[5] 同一请求连发 6 次(reasoning_content 报错与请求形态相关, 非随机)
|
|
63
|
+
======================================================================
|
|
64
|
+
第1次 OK: ''
|
|
65
|
+
第2次 OK: '我手上只有读文件的能力,'
|
|
66
|
+
第3次 OK: '好'
|
|
67
|
+
第4次 OK: '好'
|
|
68
|
+
第5次 OK: ''
|
|
69
|
+
第6次 OK: '先说清楚我这边的情况,免'
|
|
70
|
+
|
|
71
|
+
统计: OK=6 reasoning_content 400=0 其它 400=0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
======================================================================
|
|
75
|
+
[6] 上下文窗口探针(免费/必然超长 → 用于确认窗口大小)
|
|
76
|
+
======================================================================
|
|
77
|
+
探针: 竟然通过(窗口 > 1.2M tokens)
|
|
78
|
+
|
|
79
|
+
DEFAULT_SYSTEM 字符数: 2041
|
|
80
|
+
工具 schema 字符数: 20993 (47 个工具)
|
|
81
|
+
合计附加字符数 ≈ 23034 → 约 7,678 tokens
|
|
82
|
+
会话本体(实测) = 980,288 tokens; max_output_tokens = 32768
|
|
83
|
+
预估请求总量 ≈ 1,020,734 tokens
|
|
84
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# 会话"再也打不开"根因报告 — 内容里的标记被当成消息边界(2026-09-12)
|
|
2
|
+
|
|
3
|
+
> 现象: nf 启动后说什么都报 `DeepSeek 400 ... insufficient tool messages following tool_calls message`(截图同款),
|
|
4
|
+
> 即用户所说"项目打不开了"。本文记录完整证据链、修复与验证;**结论均有原始输出支撑**(见文末索引)。
|
|
5
|
+
|
|
6
|
+
## 1. 结论(一句话)
|
|
7
|
+
|
|
8
|
+
工具结果 dump 会话 `.md` 文件时, 被 dump 的 `<!-- nf-msg … -->` 标记行作为**内容**落进新会话文件;
|
|
9
|
+
解析时被全文 `findall` 解码成**额外消息**, 插进 `assistant(tool_calls)` 与它的 `tool` 响应之间 →
|
|
10
|
+
请求违反 API 的"tool 响应必须紧跟且覆盖全部 tool_call_id"→ 每轮 400。
|
|
11
|
+
而旧的修复函数 `_repair_incomplete_tail` **只修尾部**, 看不到中段违规 → 永久失败。
|
|
12
|
+
|
|
13
|
+
## 2. 定位过程(证据链)
|
|
14
|
+
|
|
15
|
+
| 步骤 | 做法 | 结果 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 1 | 解出截图报错原文 | `An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message)` |
|
|
18
|
+
| 2 | 扫描本机各项目会话, 按 API 规则校验工具序列(只读) | 只有 `NexForgeCLI/.nexforge/sessions/session-122357.md` 命中: **中段 idx=1724 违规 4 例**; CosyVoice 两个会话合法 |
|
|
19
|
+
| 3 | 打印违规点上下文 | `assistant(4 个 tool_calls)` 的响应被 4 条重复助手消息插开 |
|
|
20
|
+
| 4 | 查重复消息来源 | 文件里存在"内容里的标记": 工具结果 `read_file .nexforge/sessions/session-…md` 的 dump 里带着真标记行 |
|
|
21
|
+
| 5 | 统计幽灵消息 | 该会话多出 **10 份重复助手消息**; 修复后加载条数 3786 → 3785(少的那条正是幽灵) |
|
|
22
|
+
| 6 | 用真机 API 复现 | 修复前的形态 → **400(与截图完全一致)**; 修复后 → 200 正常回复 |
|
|
23
|
+
|
|
24
|
+
关键点: 违规在**中段**——这是"只修尾部"兜不住的原因, 也解释了为什么"历史会话能正常显示, 一说话就炸"。
|
|
25
|
+
|
|
26
|
+
## 3. 修复
|
|
27
|
+
|
|
28
|
+
| # | 位置 | 改动 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 1 | `store/session_store.py` `_escape_marker_prefix` | **写侧转义**: 人读部分里的 `<!-- nf-msg` 插零宽空格 → 内容不再具备标记形状(标记负载无损) |
|
|
31
|
+
| 2 | `store/session_store.py` `_MARKER_RE` | **读侧锚定行首**(`re.M`): 只有独立成行的标记才是边界(dump 出来的标记带 ` 12 | ` 前缀, 旧正则也匹配) |
|
|
32
|
+
| 3 | `core/session.py` `_repair_tool_sequence` | **全序列修复**(旧名保留为别名): 夹层消息保序后移、缺响应补"已取消"、重复/孤儿 tool 删除 → 修复后序列对 API **恒合法**; 调用点 `add_user` / `run_turn` / 回合结束 |
|
|
33
|
+
| 4 | `core/agent.py` | 防死循环提示改为整块工具响应**之后**追加(原先会插进 tool 之间, 同类违规) |
|
|
34
|
+
|
|
35
|
+
设计取舍与边界见 ADR: `docs/decisions/2026-09-12-内容里的标记不得当成消息边界.md`。
|
|
36
|
+
|
|
37
|
+
## 4. 验证(三档)
|
|
38
|
+
|
|
39
|
+
| 档 | 内容 | 结果 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| 单元回归 | 新增/更新 20 项: 内容里的标记往返、锚定读入、中段违规修复、幂等、合法序列不动、add_user 恢复后合法 | `tests/test_session_store.py` `tests/test_session_repair.py` `tests/test_session_invariant.py` 全绿 |
|
|
42
|
+
| 全量 + 门禁 | `pytest -q` 与 `scripts/check.py` | **577 passed / 3 skipped**;gate **6/6 PASS** |
|
|
43
|
+
| 真机 API | 合成最小复现 + **用户真机会话切片** + **整会话 3785 条端到端** | 修复前 400(截图同款) → 修复后 OK(见 `session-marker-boundary_live.log`) |
|
|
44
|
+
|
|
45
|
+
真机端到端最有力的一条: 用户会话**全部 3785 条**(体积 ≈ 980k prompt tokens)在修复后能正常得到回复,
|
|
46
|
+
说明既不是"尾部侥幸合法", 也不是"切片合法、整体不合法"。
|
|
47
|
+
|
|
48
|
+
**用户侧确认(2026-09-12)**: 该项目会话已恢复正常, 可继续对话(修复后首个回合由用户实机跑通)。
|
|
49
|
+
|
|
50
|
+
## 5. 排查中的两个坑(留给后来者)
|
|
51
|
+
|
|
52
|
+
1. **请求不能以 assistant 消息结尾**: thinking 模式下 DeepSeek 会报
|
|
53
|
+
`The reasoning_content in the thinking mode must be passed back to the API.`
|
|
54
|
+
`_repair_tool_sequence` 为兼容旧行为会**弹出尾部未配对 user**; 若在"修复后"自行构造请求
|
|
55
|
+
(而不是走 `run_turn` 的 `修复 → add_user` 顺序), 就会得到一个以 assistant 结尾的列表 →
|
|
56
|
+
触发这条 400, 看起来像"修复没生效"。**app 的真实路径永远以 user 结尾**, 不受影响。
|
|
57
|
+
排查时若看到这条错, 先看请求最后一条是不是 assistant。
|
|
58
|
+
2. **同一请求重复 6 次全 OK**: 上面的 reasoning 报错与请求形态强相关(见坑 1), 不是服务端随机抖动;
|
|
59
|
+
排查"偶发"问题时先做 A/B 并打印规范形态哈希, 别急着下"服务端不稳定"的结论。
|
|
60
|
+
|
|
61
|
+
## 6. 遗留与建议(本次未改)
|
|
62
|
+
|
|
63
|
+
| 项 | 现状 | 建议 |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| 会话里已积累的 10 份重复助手消息 | 修复只**后移**不删除(不变量: 不允许无声丢失); 请求合法但占上下文 | 首次成功回合后 app 会重写会话文件; 重复内容如需清理请人工处理 |
|
|
66
|
+
| 会话体量 ≈ 980k tokens | 实测: 会话本体 980,288 tokens; 加上 `DEFAULT_SYSTEM` + 47 个工具 schema(≈7.7k)+ `max_output_tokens`(32,768)≈ **1.02M**; 上下文窗口探针(1.25M tokens 的超长请求)通过 ⇒ 目前仍能发出, 但**已无余量** | 上下文治理只按"轮数 > 60"压缩、没有按 token 的裁剪 → 优先补"按 token 裁剪/强制压缩"; 短期内该项目建议 `/new` 开新会话 |
|
|
67
|
+
| 失败的回合会丢弃那条用户消息 | 观测: 会话文件在 01:16 被(带本修复的)nf 重写, 序列已合法(非法点 0), 但尾部**没有**新用户消息与回复 → 说明那一轮以失败/中断收尾, 且 `_repair_tool_sequence` 的"弹尾部未配对 user"(沿用旧行为)把该用户消息从会话里移除了 | 若是"请求已发出但没成", `_repair_tool_sequence` 弹 user 这一步可考虑改为不弹(API 允许连续 user 消息); 需产品决策, 本次未改 |
|
|
68
|
+
| 陈旧分段残留 | `save_session` 不清理"超出新段数的旧分段", 而 `load_session` 按序号连续读 → 列表变短(压缩)后旧内容被追加(已复现: 3 条被加载成 130 条)。全序列修复能兜住 400, 但内容冗余仍在 | 会话文件头声明分段数(`> 分段 N`), 加载按声明数读取, 超出的不读也不删 |
|
|
69
|
+
|
|
70
|
+
## 7. 证据索引
|
|
71
|
+
|
|
72
|
+
- `docs/evidence/session-marker-boundary.log` — 脚本级原始输出(旧行为 vs 修复后、真机会话校验、陈旧分段模拟)
|
|
73
|
+
- `docs/evidence/session-marker-boundary_live.log` — 真机 API 原始输出(含 980k tokens 端到端)
|
|
74
|
+
- `docs/evidence/session-marker-boundary_fulltest.log` — 全量测试输出
|
|
75
|
+
- 回归测试: `tests/test_session_store.py::test_roundtrip_does_not_revive_marker_inside_content`、
|
|
76
|
+
`tests/test_session_store.py::test_load_ignores_marker_line_with_dump_prefix`、
|
|
77
|
+
`tests/test_session_repair.py::test_repair_mid_list_violation_keeps_responses_adjacent`
|
|
@@ -92,10 +92,11 @@ class AgentLoop:
|
|
|
92
92
|
self._approve = approve
|
|
93
93
|
|
|
94
94
|
async def run_turn(self, user_text: str) -> AsyncIterator[AgentEvent]:
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
# 兜底:全序列修复 —— 上次中断残留(超时/取消/异常)与历史文件里的
|
|
96
|
+
# 非法错位都在此消除, 否则 assistant(tool_calls) 不紧跟 tool 响应
|
|
97
|
+
# → DeepSeek 400 罢工(每轮都失败)
|
|
98
|
+
from nexforge.core.session import _repair_tool_sequence
|
|
99
|
+
_repair_tool_sequence(self.session.messages)
|
|
99
100
|
self.session.add_user(user_text)
|
|
100
101
|
last_failed: tuple[str, str] = ("", "")
|
|
101
102
|
consecutive_fails = 0
|
|
@@ -137,6 +138,10 @@ class AgentLoop:
|
|
|
137
138
|
yield AgentEvent(AgentEventKind.TURN_DONE, message=final_msg)
|
|
138
139
|
return
|
|
139
140
|
|
|
141
|
+
# 防死循环提示必须**整块工具响应之后**追加: 混在 tool 消息之间
|
|
142
|
+
# 会让 assistant(tool_calls) 的响应不连续(API 要求紧跟),
|
|
143
|
+
# 与 session._repair_tool_sequence 的判据同源。
|
|
144
|
+
loop_hints: list[str] = []
|
|
140
145
|
for tc in final_msg.tool_calls:
|
|
141
146
|
args = self.registry.parse_args(tc.arguments)
|
|
142
147
|
tool = self.registry.get(tc.name)
|
|
@@ -158,13 +163,11 @@ class AgentLoop:
|
|
|
158
163
|
if consecutive_fails >= 3:
|
|
159
164
|
# 运行地派生(D9_decision.md A5): 由失败计数重算,
|
|
160
165
|
# 属瞬态纠偏, 不随会话落盘(恢复后不再需要)。
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
),
|
|
167
|
-
})
|
|
166
|
+
# 先收集, 循环结束后统一追加(见上: 不得插进 tool 之间)
|
|
167
|
+
loop_hints.append(
|
|
168
|
+
f"[防死循环] 工具 {tc.name} 已连续失败 {consecutive_fails} 次。"
|
|
169
|
+
"请不要重复相同调用;换一种方法或跳过当前步骤,如无法完成则向用户说明原因并结束。"
|
|
170
|
+
)
|
|
168
171
|
consecutive_fails = 0 # 复位,避免重复注入
|
|
169
172
|
else:
|
|
170
173
|
# 成功 → 重置失败跟踪
|
|
@@ -176,6 +179,8 @@ class AgentLoop:
|
|
|
176
179
|
tool_ok=result.ok,
|
|
177
180
|
tool_summary=result.summary or result.error or result.output[:60],
|
|
178
181
|
)
|
|
182
|
+
for hint in loop_hints:
|
|
183
|
+
self.session.messages.append({"role": "system", "content": hint})
|
|
179
184
|
|
|
180
185
|
yield AgentEvent(
|
|
181
186
|
AgentEventKind.ERROR,
|