minimal-agent 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/package.json +52 -5
- package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
- package/plugins/computer_use/README.md +94 -0
- package/plugins/computer_use/app/_template.md +142 -0
- package/plugins/computer_use/app/chrome.md +165 -0
- package/plugins/computer_use/app/finder.md +76 -0
- package/plugins/computer_use/app/macos-system-settings.md +81 -0
- package/plugins/computer_use/app/microsoft-excel.md +80 -0
- package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
- package/plugins/computer_use/app/microsoft-word.md +79 -0
- package/plugins/computer_use/app/wechat.md +413 -0
- package/plugins/computer_use/app/weread.md +319 -0
- package/plugins/computer_use/commands/computeruse.md +25 -0
- package/plugins/computer_use/commands/cu.md +9 -0
- package/plugins/computer_use/plugin.js +26 -0
- package/plugins/computer_use/plugin.ts +30 -0
- package/plugins/computer_use/src/capture.js +90 -0
- package/plugins/computer_use/src/capture.ts +104 -0
- package/plugins/computer_use/src/commands.js +91 -0
- package/plugins/computer_use/src/commands.ts +103 -0
- package/plugins/computer_use/src/daemon.js +134 -0
- package/plugins/computer_use/src/daemon.ts +161 -0
- package/plugins/computer_use/src/manuals.js +135 -0
- package/plugins/computer_use/src/manuals.ts +159 -0
- package/plugins/computer_use/src/verify.js +168 -0
- package/plugins/computer_use/src/verify.ts +219 -0
- package/plugins/computer_use/test/commands.test.ts +73 -0
- package/plugins/cron-runner/plugin.ts +38 -0
- package/plugins/cron-runner/src/commands.ts +373 -0
- package/plugins/cron-runner/src/cronTime.ts +155 -0
- package/plugins/cron-runner/src/lock.ts +79 -0
- package/plugins/cron-runner/src/runs.ts +99 -0
- package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
- package/plugins/cron-runner/src/scheduler.ts +376 -0
- package/plugins/cron-runner/src/schema.ts +139 -0
- package/plugins/cron-runner/src/service.ts +193 -0
- package/plugins/cron-runner/src/store.ts +333 -0
- package/plugins/cron-runner/src/types.ts +141 -0
- package/plugins/cron-runner/test/cronTime.test.ts +41 -0
- package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
- package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
- package/plugins/cron-runner/test/service.test.ts +76 -0
- package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
- package/plugins/workflow-runner/plugin.js +56 -2
- package/plugins/workflow-runner/plugin.ts +113 -0
- package/plugins/workflow-runner/src/agentCommon.js +43 -4
- package/plugins/workflow-runner/src/agentCommon.ts +435 -0
- package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
- package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
- package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
- package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
- package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
- package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
- package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
- package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
- package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
- package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
- package/plugins/workflow-runner/src/dslManifest.ts +127 -0
- package/plugins/workflow-runner/src/expressions.ts +622 -0
- package/plugins/workflow-runner/src/index.js +13 -4
- package/plugins/workflow-runner/src/index.ts +338 -0
- package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
- package/plugins/workflow-runner/src/loader.js +23 -0
- package/plugins/workflow-runner/src/loader.ts +721 -0
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +64 -0
- package/plugins/workflow-runner/src/recovery.ts +149 -0
- package/plugins/workflow-runner/src/runner.js +75 -659
- package/plugins/workflow-runner/src/runner.ts +604 -0
- package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
- package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
- package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
- package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
- package/plugins/workflow-runner/src/stepPipeline.js +277 -0
- package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
- package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +37 -1
- package/plugins/workflow-runner/src/types.ts +602 -0
- package/plugins/workflow-runner/src/workflowState.ts +733 -0
- package/plugins/workflow-runner/test/agent.test.ts +587 -0
- package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
- package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
- package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
- package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
- package/plugins/workflow-runner/test/context-files.test.ts +320 -0
- package/plugins/workflow-runner/test/e2e.test.ts +370 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
- package/plugins/workflow-runner/test/expressions.test.ts +397 -0
- package/plugins/workflow-runner/test/foundation.test.ts +126 -0
- package/plugins/workflow-runner/test/graceful.test.ts +139 -0
- package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
- package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
- package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
- package/plugins/workflow-runner/test/loader.test.ts +617 -0
- package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
- package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
- package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
- package/plugins/workflow-runner/test/recovery.test.ts +265 -0
- package/plugins/workflow-runner/test/runner.test.ts +911 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
- package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
- package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
- package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
- package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
- package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
- package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
- package/plugins/workflow-runner/test/word.load.test.ts +208 -0
- package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
- package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
- package/skills/browser-automation/SKILL.md +9 -19
- package/skills/web-crawler/SKILL.md +9 -11
- package/src/askBridge.ts +249 -0
- package/src/bootstrap/cwdArg.js +10 -3
- package/src/bootstrap/cwdArg.ts +31 -0
- package/src/bootstrap/workingDir.ts +48 -0
- package/src/cli/args.js +70 -7
- package/src/cli/args.ts +146 -0
- package/src/cli/computerUse.js +185 -0
- package/src/cli/computerUse.ts +221 -0
- package/src/cli/configMenu.js +274 -202
- package/src/cli/configMenu.tsx +521 -0
- package/src/cli/memoryAccess.js +45 -0
- package/src/cli/memoryAccess.ts +53 -0
- package/src/cli/print.js +119 -5
- package/src/cli/print.ts +597 -0
- package/src/cli/quickModelSetup.js +60 -0
- package/src/cli/quickModelSetup.tsx +96 -0
- package/src/cli/streamJson.js +80 -4
- package/src/cli/streamJson.ts +433 -0
- package/src/cli/wizards/advisorWizard.js +1 -0
- package/src/cli/wizards/advisorWizard.tsx +195 -0
- package/src/cli/wizards/agentRuntimeWizard.js +84 -0
- package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
- package/src/cli/wizards/browserWizard.js +55 -0
- package/src/cli/wizards/browserWizard.tsx +77 -0
- package/src/cli/wizards/contextAdvisorWizard.js +84 -0
- package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
- package/src/cli/wizards/fieldForm.js +7 -4
- package/src/cli/wizards/fieldForm.tsx +250 -0
- package/src/cli/wizards/imageWizard.js +4 -5
- package/src/cli/wizards/imageWizard.tsx +96 -0
- package/src/cli/wizards/openRouterWizard.tsx +48 -0
- package/src/cli/wizards/providerPresets.js +1 -8
- package/src/cli/wizards/providerPresets.ts +106 -0
- package/src/cli/wizards/providerWizard.js +152 -205
- package/src/cli/wizards/providerWizard.tsx +266 -0
- package/src/cli/wizards/searchWizard.tsx +47 -0
- package/src/cli/wizards/ttsWizard.js +4 -3
- package/src/cli/wizards/ttsWizard.tsx +61 -0
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configDiagnostics.js +117 -0
- package/src/config/configDiagnostics.ts +166 -0
- package/src/config/configFile.js +52 -32
- package/src/config/configFile.ts +633 -0
- package/src/config/configManagement.js +126 -0
- package/src/config/configManagement.ts +159 -0
- package/src/config/configService.js +66 -11
- package/src/config/configService.ts +554 -0
- package/src/config/numericLimits.js +61 -0
- package/src/config/numericLimits.ts +80 -0
- package/src/config.ts +109 -0
- package/src/context/agentHome.ts +11 -0
- package/src/context/archive.ts +144 -0
- package/src/context/budgetPlan.js +54 -0
- package/src/context/budgetPlan.ts +88 -0
- package/src/context/compact.js +179 -106
- package/src/context/compact.ts +584 -0
- package/src/context/contextBundle.js +119 -0
- package/src/context/contextBundle.ts +176 -0
- package/src/context/contextLock.ts +84 -0
- package/src/context/microCompactLite.ts +218 -0
- package/src/context/persistContext.ts +177 -0
- package/src/context/reactiveCompact.js +19 -4
- package/src/context/reactiveCompact.ts +240 -0
- package/src/context/recentDirs.ts +42 -0
- package/src/context/sessionPath.ts +206 -0
- package/src/context/sessionRegistry.js +8 -2
- package/src/context/sessionRegistry.ts +414 -0
- package/src/context/snipCompact.ts +163 -0
- package/src/context/tokenCounter.ts +106 -0
- package/src/context/workspaceIndex.js +2 -2
- package/src/context/workspaceIndex.ts +751 -0
- package/src/entry.compiled.tsx +20 -0
- package/src/harness/claimValidation.js +9 -0
- package/src/harness/claimValidation.ts +22 -0
- package/src/harness/evidenceLedger.js +92 -0
- package/src/harness/evidenceLedger.ts +96 -0
- package/src/harness/stallGuard.js +60 -0
- package/src/harness/stallGuard.ts +81 -0
- package/src/humanIntervention.js +129 -0
- package/src/humanIntervention.ts +216 -0
- package/src/infra/verification.ts +295 -0
- package/src/irreversible/rules.js +216 -0
- package/src/irreversible/rules.ts +261 -0
- package/src/llm/client.js +40 -3
- package/src/llm/client.ts +621 -0
- package/src/llm/promptCache.js +27 -0
- package/src/llm/promptCache.ts +41 -0
- package/src/llm/providers/anthropicFormat.js +41 -5
- package/src/llm/providers/anthropicFormat.ts +290 -0
- package/src/llm/providers/openaiFormat.js +186 -8
- package/src/llm/providers/openaiFormat.ts +428 -0
- package/src/loop.js +614 -63
- package/src/loop.ts +1454 -0
- package/src/main.js +67 -10
- package/src/main.tsx +344 -0
- package/src/memory/codec.js +204 -0
- package/src/memory/codec.ts +249 -0
- package/src/memory/constants.js +34 -0
- package/src/memory/constants.ts +50 -0
- package/src/memory/episodicMemory.js +684 -0
- package/src/memory/episodicMemory.ts +815 -0
- package/src/memory/fileStore.js +148 -0
- package/src/memory/fileStore.ts +184 -0
- package/src/memory/memoryService.js +570 -0
- package/src/memory/memoryService.ts +859 -0
- package/src/memory/migration.js +128 -0
- package/src/memory/migration.ts +160 -0
- package/src/memory/repository.js +789 -0
- package/src/memory/repository.ts +893 -0
- package/src/memory/retrieval.js +108 -0
- package/src/memory/retrieval.ts +145 -0
- package/src/memory/searchIndex.js +107 -0
- package/src/memory/searchIndex.ts +153 -0
- package/src/memory/topicView.js +82 -0
- package/src/memory/topicView.ts +95 -0
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.js +2 -0
- package/src/memory/types.ts +186 -0
- package/src/messageQueue.ts +123 -0
- package/src/multimodal.js +128 -46
- package/src/multimodal.ts +349 -0
- package/src/plugin-sdk.js +21 -1
- package/src/plugin-sdk.ts +134 -0
- package/src/plugins/builtinPluginIds.js +47 -0
- package/src/plugins/builtinPluginIds.ts +51 -0
- package/src/plugins/builtinRegistry.ts +43 -0
- package/src/plugins/commandRouter.js +40 -26
- package/src/plugins/commandRouter.ts +261 -0
- package/src/plugins/hookEngine.ts +341 -0
- package/src/plugins/pluginApi.ts +133 -0
- package/src/plugins/pluginLifecycle.ts +34 -0
- package/src/plugins/pluginLoader.ts +147 -0
- package/src/plugins/pluginManifest.js +127 -0
- package/src/plugins/pluginManifest.ts +152 -0
- package/src/plugins/pluginRunner.js +47 -2
- package/src/plugins/pluginRunner.ts +207 -0
- package/src/plugins/transcript.ts +208 -0
- package/src/prompts/lint.js +169 -0
- package/src/prompts/lint.ts +220 -0
- package/src/prompts/pluginList.js +32 -16
- package/src/prompts/pluginList.ts +86 -0
- package/src/prompts/policyRegistry.js +318 -0
- package/src/prompts/policyRegistry.ts +358 -0
- package/src/prompts/projectInstructions.js +24 -0
- package/src/prompts/projectInstructions.ts +81 -0
- package/src/prompts/promptAssembly.js +311 -202
- package/src/prompts/promptAssembly.ts +510 -0
- package/src/prompts/skillList.js +106 -45
- package/src/prompts/skillList.ts +198 -0
- package/src/prompts/system.js +31 -6
- package/src/prompts/system.ts +81 -0
- package/src/runtime/agentRuntimeContext.js +53 -0
- package/src/runtime/agentRuntimeContext.ts +112 -0
- package/src/session/runAgentTurn.js +71 -12
- package/src/session/runAgentTurn.ts +181 -0
- package/src/session/runTurn.js +29 -0
- package/src/session/runTurn.ts +160 -0
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +79 -3
- package/src/session/sessionState.ts +134 -0
- package/src/toolGate.ts +50 -0
- package/src/tools/advisor/advisor.js +76 -15
- package/src/tools/advisor/advisor.ts +512 -0
- package/src/tools/agent/agent.js +245 -29
- package/src/tools/agent/agent.ts +837 -0
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +20 -8
- package/src/tools/agent/agentTypes.ts +94 -0
- package/src/tools/agent/coordinator.ts +111 -0
- package/src/tools/agent/worktree.ts +296 -0
- package/src/tools/ask/ask.js +17 -0
- package/src/tools/ask/ask.ts +122 -0
- package/src/tools/bash/attachImage.js +223 -0
- package/src/tools/bash/attachImage.ts +281 -0
- package/src/tools/bash/bash.js +132 -14
- package/src/tools/bash/bash.ts +642 -0
- package/src/tools/bash/semantics.ts +103 -0
- package/src/tools/bash/warnings.js +17 -11
- package/src/tools/bash/warnings.ts +117 -0
- package/src/tools/capabilities.js +42 -0
- package/src/tools/capabilities.ts +69 -0
- package/src/tools/computeruse/accessibilityPermission.js +261 -0
- package/src/tools/computeruse/accessibilityPermission.ts +294 -0
- package/src/tools/computeruse/axSearch.js +96 -0
- package/src/tools/computeruse/axSearch.ts +120 -0
- package/src/tools/computeruse/axSnap.js +272 -0
- package/src/tools/computeruse/axSnap.ts +349 -0
- package/src/tools/computeruse/capabilityLabels.js +95 -0
- package/src/tools/computeruse/capabilityLabels.ts +115 -0
- package/src/tools/computeruse/computerUseService.js +2235 -0
- package/src/tools/computeruse/computerUseService.ts +2686 -0
- package/src/tools/computeruse/computeruse.js +2742 -0
- package/src/tools/computeruse/computeruse.ts +3265 -0
- package/src/tools/computeruse/diagnosticTrace.js +154 -0
- package/src/tools/computeruse/diagnosticTrace.ts +195 -0
- package/src/tools/computeruse/diagnostics.js +369 -0
- package/src/tools/computeruse/diagnostics.ts +453 -0
- package/src/tools/computeruse/grants.js +53 -0
- package/src/tools/computeruse/grants.ts +70 -0
- package/src/tools/computeruse/humanBoundary.js +159 -0
- package/src/tools/computeruse/humanBoundary.ts +199 -0
- package/src/tools/computeruse/intervention.js +77 -0
- package/src/tools/computeruse/intervention.ts +110 -0
- package/src/tools/computeruse/manualSchema.js +709 -0
- package/src/tools/computeruse/manualSchema.ts +916 -0
- package/src/tools/computeruse/observationImages.js +705 -0
- package/src/tools/computeruse/observationImages.ts +889 -0
- package/src/tools/computeruse/protocol.js +60 -0
- package/src/tools/computeruse/protocol.ts +515 -0
- package/src/tools/computeruse/telemetryLog.js +57 -0
- package/src/tools/computeruse/telemetryLog.ts +67 -0
- package/src/tools/computeruse/vision/appGuide.js +325 -0
- package/src/tools/computeruse/vision/appGuide.ts +482 -0
- package/src/tools/computeruse/vision/focus.js +291 -0
- package/src/tools/computeruse/vision/focus.ts +382 -0
- package/src/tools/computeruse/vision/frames.js +248 -0
- package/src/tools/computeruse/vision/frames.ts +329 -0
- package/src/tools/computeruse/vision/geometry.js +131 -0
- package/src/tools/computeruse/vision/geometry.ts +163 -0
- package/src/tools/computeruse/vision/imaging.js +201 -0
- package/src/tools/computeruse/vision/imaging.ts +281 -0
- package/src/tools/computeruse/vision/png.js +274 -0
- package/src/tools/computeruse/vision/png.ts +317 -0
- package/src/tools/computeruse/vision/raster.js +320 -0
- package/src/tools/computeruse/vision/raster.ts +408 -0
- package/src/tools/computeruse/vision/settle.js +131 -0
- package/src/tools/computeruse/vision/settle.ts +220 -0
- package/src/tools/computeruse/vision/telemetry.js +196 -0
- package/src/tools/computeruse/vision/telemetry.ts +241 -0
- package/src/tools/computeruse/visionOcr.js +115 -0
- package/src/tools/computeruse/visionOcr.ts +128 -0
- package/src/tools/computeruse/waitFor.js +237 -0
- package/src/tools/computeruse/waitFor.ts +291 -0
- package/src/tools/download/download.js +10 -6
- package/src/tools/download/download.ts +187 -0
- package/src/tools/edit/edit.js +30 -18
- package/src/tools/edit/edit.ts +349 -0
- package/src/tools/edit/multi-edit.js +25 -14
- package/src/tools/edit/multi-edit.ts +223 -0
- package/src/tools/effectiveToolSet.js +73 -0
- package/src/tools/effectiveToolSet.ts +91 -0
- package/src/tools/genimage/genimage.ts +257 -0
- package/src/tools/genspeech/genspeech.ts +83 -0
- package/src/tools/glob/glob.js +176 -29
- package/src/tools/glob/glob.ts +283 -0
- package/src/tools/grep/grep.js +61 -19
- package/src/tools/grep/grep.ts +239 -0
- package/src/tools/grep/rgPath.ts +206 -0
- package/src/tools/index.js +99 -5
- package/src/tools/index.ts +293 -0
- package/src/tools/lsp/config.js +113 -0
- package/src/tools/lsp/config.ts +113 -0
- package/src/tools/lsp/lsp.js +256 -0
- package/src/tools/lsp/lsp.ts +274 -0
- package/src/tools/lsp/service.js +303 -0
- package/src/tools/lsp/service.ts +296 -0
- package/src/tools/memory/memory.js +520 -0
- package/src/tools/memory/memory.ts +625 -0
- package/src/tools/memory/policy.js +53 -0
- package/src/tools/memory/policy.ts +72 -0
- package/src/tools/notebook/notebookEdit.js +114 -0
- package/src/tools/notebook/notebookEdit.ts +126 -0
- package/src/tools/powershell/powershell.js +210 -0
- package/src/tools/powershell/powershell.ts +239 -0
- package/src/tools/presentresult/presentresult.js +145 -0
- package/src/tools/presentresult/presentresult.ts +156 -0
- package/src/tools/read/read.js +371 -163
- package/src/tools/read/read.ts +558 -0
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +51 -0
- package/src/tools/shared/fileActivity.ts +66 -0
- package/src/tools/shared/fileMutationBus.js +13 -0
- package/src/tools/shared/fileMutationBus.ts +14 -0
- package/src/tools/shared/fileState.js +73 -65
- package/src/tools/shared/fileState.ts +145 -0
- package/src/tools/shared/fileUtils.js +81 -4
- package/src/tools/shared/fileUtils.ts +504 -0
- package/src/tools/shared/killTree.ts +50 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/schemas.ts +20 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +275 -0
- package/src/tools/tasks/backgroundTaskService.ts +369 -0
- package/src/tools/tasks/processEvidence.js +24 -0
- package/src/tools/tasks/processEvidence.ts +34 -0
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +113 -0
- package/src/tools/tasks/taskOutput.ts +145 -0
- package/src/tools/tasks/taskStop.js +74 -0
- package/src/tools/tasks/taskStop.ts +99 -0
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/types.js +1 -1
- package/src/tools/types.ts +24 -0
- package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
- package/src/tools/webbrowser/browser.js +214 -11
- package/src/tools/webbrowser/browser.ts +293 -0
- package/src/tools/webbrowser/browserControl.js +153 -48
- package/src/tools/webbrowser/browserControl.ts +262 -0
- package/src/tools/webbrowser/browserDaemon.js +147 -39
- package/src/tools/webbrowser/browserDaemon.ts +270 -0
- package/src/tools/webbrowser/browserOutcome.js +273 -0
- package/src/tools/webbrowser/browserOutcome.ts +306 -0
- package/src/tools/webbrowser/browserRunRegistry.js +150 -0
- package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
- package/src/tools/webbrowser/browserService.js +20 -6
- package/src/tools/webbrowser/browserService.ts +56 -0
- package/src/tools/webbrowser/browserStream.js +30 -1
- package/src/tools/webbrowser/browserStream.ts +216 -0
- package/src/tools/webbrowser/browserTabs.js +143 -0
- package/src/tools/webbrowser/browserTabs.ts +210 -0
- package/src/tools/webbrowser/webbrowser.js +831 -150
- package/src/tools/webbrowser/webbrowser.ts +1537 -0
- package/src/tools/webfetch/preapproved.ts +274 -0
- package/src/tools/webfetch/webfetch.js +24 -23
- package/src/tools/webfetch/webfetch.ts +383 -0
- package/src/tools/websearch/websearch.js +12 -11
- package/src/tools/websearch/websearch.ts +292 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +27 -44
- package/src/tools/write/write.ts +136 -0
- package/src/types.ts +960 -0
- package/src/ui/App.js +216 -12
- package/src/ui/App.tsx +879 -0
- package/src/ui/AskQuestion.tsx +300 -0
- package/src/ui/InputBox.js +77 -1
- package/src/ui/InputBox.tsx +567 -0
- package/src/ui/LiveArea.tsx +344 -0
- package/src/ui/MessageList.js +27 -6
- package/src/ui/MessageList.tsx +559 -0
- package/src/ui/ModelPicker.tsx +107 -0
- package/src/ui/QueuedMessagesBanner.tsx +51 -0
- package/src/ui/Root.js +6 -8
- package/src/ui/Root.tsx +100 -0
- package/src/ui/SessionPicker.tsx +169 -0
- package/src/ui/StatusLine.js +22 -1
- package/src/ui/StatusLine.tsx +145 -0
- package/src/ui/WorkspacePicker.tsx +84 -0
- package/src/ui/advisorCommand.js +54 -0
- package/src/ui/advisorCommand.ts +63 -0
- package/src/ui/computerCommand.js +19 -0
- package/src/ui/computerCommand.ts +35 -0
- package/src/ui/figures.ts +45 -0
- package/src/ui/hooks/askSlot.ts +112 -0
- package/src/ui/hooks/useChat.js +271 -7
- package/src/ui/hooks/useChat.ts +1578 -0
- package/src/ui/hooks/useInputHistory.ts +235 -0
- package/src/ui/hooks/usePasteHandler.ts +220 -0
- package/src/ui/hooks/useTerminalSize.ts +41 -0
- package/src/ui/hooks/useTextBuffer.ts +108 -0
- package/src/ui/hooks/useTokenUsage.ts +44 -0
- package/src/ui/liveViewport.ts +189 -0
- package/src/ui/markdown.ts +230 -0
- package/src/ui/textBuffer.ts +239 -0
- package/src/ui/theme.ts +35 -0
- package/src/utils/clipboardImage.js +358 -0
- package/src/utils/clipboardImage.ts +420 -0
- package/src/utils/debugLog.ts +50 -0
- package/src/utils/greenRoot.ts +35 -0
- package/src/utils/packageRoot.ts +40 -0
- package/src/utils/pathContains.js +28 -0
- package/src/utils/pathContains.ts +31 -0
- package/src/utils/resourcePaths.ts +71 -0
- package/src/utils/stripThink.ts +49 -0
- package/src/utils/zodToJson.ts +73 -0
- package/src/web/browserBridge.js +365 -66
- package/src/web/browserBridge.ts +672 -0
- package/src/web/protocol.ts +1804 -0
- package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
- package/workflows/ppt.yaml +13 -2
- package/workflows/wechat-reading-shots.yaml +315 -0
- package/workflows/word.yaml +11 -1
- package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
- /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
- /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import { pathContains } from '../utils/pathContains.js';
|
|
7
|
+
|
|
8
|
+
import { parseMemoryRecord, serializeMemoryRecord, sha256 } from './codec.js';
|
|
9
|
+
import { ALL_CATEGORIES, CATEGORY_DIR } from './constants.js';
|
|
10
|
+
import {
|
|
11
|
+
acquireFileLock,
|
|
12
|
+
atomicWriteFile,
|
|
13
|
+
memoryRecordPath,
|
|
14
|
+
scopeDirectory,
|
|
15
|
+
} from './fileStore.js';
|
|
16
|
+
import type {
|
|
17
|
+
MemoryDiagnostic,
|
|
18
|
+
MemoryRecordV2,
|
|
19
|
+
MemoryScope,
|
|
20
|
+
MemorySnapshot,
|
|
21
|
+
MemoryVersion,
|
|
22
|
+
VersionedMemoryRecord,
|
|
23
|
+
} from './types.js';
|
|
24
|
+
import { ensureTopicView } from './topicView.js';
|
|
25
|
+
|
|
26
|
+
interface CachedFile {
|
|
27
|
+
mtimeMs: number;
|
|
28
|
+
size: number;
|
|
29
|
+
version: MemoryVersion;
|
|
30
|
+
record?: MemoryRecordV2;
|
|
31
|
+
diagnostic?: MemoryDiagnostic;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fileCache = new Map<string, CachedFile>();
|
|
35
|
+
/** 兜底审计的最小间隔:watcher 漏掉的变更最迟这么久后会被 stat 审计发现。 */
|
|
36
|
+
const SCOPE_AUDIT_TTL_MS = 1_000;
|
|
37
|
+
/** 「扫描期间被改动 → 重扫」的重试上限,防止持续写入把读拖成活锁。 */
|
|
38
|
+
const MAX_SCAN_RETRIES = 3;
|
|
39
|
+
const MAX_SCOPE_CACHE_ENTRIES = 128;
|
|
40
|
+
|
|
41
|
+
const repositoryDiagnostics = {
|
|
42
|
+
snapshotHits: 0,
|
|
43
|
+
watchInvalidations: 0,
|
|
44
|
+
fallbackAudits: 0,
|
|
45
|
+
fullScans: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function memoryRepositoryDiagnostics(): Readonly<typeof repositoryDiagnostics> {
|
|
49
|
+
return { ...repositoryDiagnostics };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resetMemoryRepositoryDiagnostics(): void {
|
|
53
|
+
repositoryDiagnostics.snapshotHits = 0;
|
|
54
|
+
repositoryDiagnostics.watchInvalidations = 0;
|
|
55
|
+
repositoryDiagnostics.fallbackAudits = 0;
|
|
56
|
+
repositoryDiagnostics.fullScans = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ScopeCacheEntry {
|
|
60
|
+
snapshot: MemorySnapshot;
|
|
61
|
+
dirty: boolean;
|
|
62
|
+
directoryFingerprint: string;
|
|
63
|
+
lastAuditAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ScopeRuntimeState {
|
|
67
|
+
cache?: ScopeCacheEntry;
|
|
68
|
+
watchers: FSWatcher[];
|
|
69
|
+
watcherComplete: boolean;
|
|
70
|
+
watcherRefreshRequired: boolean;
|
|
71
|
+
epoch: number;
|
|
72
|
+
lastUsedAt: number;
|
|
73
|
+
lastDrainAt: number;
|
|
74
|
+
suppressDirtyUntil: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scopeStates = new Map<string, ScopeRuntimeState>();
|
|
78
|
+
|
|
79
|
+
function scopeState(key: string): ScopeRuntimeState {
|
|
80
|
+
let state = scopeStates.get(key);
|
|
81
|
+
if (!state) {
|
|
82
|
+
state = {
|
|
83
|
+
watchers: [],
|
|
84
|
+
watcherComplete: false,
|
|
85
|
+
watcherRefreshRequired: false,
|
|
86
|
+
epoch: 0,
|
|
87
|
+
lastUsedAt: 0,
|
|
88
|
+
lastDrainAt: 0,
|
|
89
|
+
suppressDirtyUntil: 0,
|
|
90
|
+
};
|
|
91
|
+
scopeStates.set(key, state);
|
|
92
|
+
}
|
|
93
|
+
return state;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** scope 目录下由本模块自己生成、不该被当成「用户改了记忆」的条目。 */
|
|
97
|
+
const GENERATED_SCOPE_ENTRIES: ReadonlySet<string> = new Set([
|
|
98
|
+
'views',
|
|
99
|
+
'.transactions',
|
|
100
|
+
'.records.lock',
|
|
101
|
+
]);
|
|
102
|
+
const CATEGORY_SCOPE_ENTRIES = new Set(Object.values(CATEGORY_DIR));
|
|
103
|
+
const WATCHER_SETTLE_REUSE_MS = 50;
|
|
104
|
+
const WATCHER_SETTLE_WAIT_MS = 20;
|
|
105
|
+
const INTERNAL_RECORD_EVENT_TTL_MS = 1_000;
|
|
106
|
+
const internalRecordEvents = new Map<string, number>();
|
|
107
|
+
|
|
108
|
+
function markInternalRecordEvent(path: string): void {
|
|
109
|
+
internalRecordEvents.set(path, Date.now() + INTERNAL_RECORD_EVENT_TTL_MS);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function consumeInternalRecordEvent(path: string): boolean {
|
|
113
|
+
const expiresAt = internalRecordEvents.get(path);
|
|
114
|
+
if (!expiresAt) return false;
|
|
115
|
+
if (expiresAt < Date.now()) {
|
|
116
|
+
internalRecordEvents.delete(path);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
internalRecordEvents.delete(path);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface MemoryTransactionItem {
|
|
124
|
+
id: string;
|
|
125
|
+
oldPath: string;
|
|
126
|
+
nextPath: string;
|
|
127
|
+
stagePath: string;
|
|
128
|
+
expectedVersion: MemoryVersion;
|
|
129
|
+
targetVersion: MemoryVersion;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface MemoryTransactionManifest {
|
|
133
|
+
schemaVersion: 1;
|
|
134
|
+
id: string;
|
|
135
|
+
items: MemoryTransactionItem[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function transactionRoot(base: string): string { return join(base, '.transactions'); }
|
|
139
|
+
function transactionManifestPath(base: string): string { return join(transactionRoot(base), 'current.json'); }
|
|
140
|
+
function generationPath(base: string): string { return join(transactionRoot(base), 'generation'); }
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 「这个路径在不在那个目录里」只有一份实现:`src/utils/pathContains.ts`。
|
|
144
|
+
* 这里只负责把两边解析成绝对路径(manifest 里的路径来自 JSON,可能是相对的),
|
|
145
|
+
* 判据本身不再手写第五遍——那个 util 的头注释专门警告过手写副本会各自漂移。
|
|
146
|
+
*/
|
|
147
|
+
function pathInside(base: string, path: string): boolean {
|
|
148
|
+
return pathContains(resolve(base), resolve(path));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readGeneration(base: string): Promise<string> {
|
|
152
|
+
return readFile(generationPath(base), 'utf8').catch(() => '');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function readTransactionManifest(base: string): Promise<MemoryTransactionManifest | null> {
|
|
156
|
+
const raw = await readFile(transactionManifestPath(base), 'utf8').catch((error) => {
|
|
157
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
158
|
+
throw error;
|
|
159
|
+
});
|
|
160
|
+
if (raw === null) return null;
|
|
161
|
+
let parsed: MemoryTransactionManifest;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(raw) as MemoryTransactionManifest;
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error('MEMORY_TRANSACTION_CORRUPT');
|
|
166
|
+
}
|
|
167
|
+
if (parsed.schemaVersion !== 1 || !parsed.id || !Array.isArray(parsed.items)) {
|
|
168
|
+
throw new Error('MEMORY_TRANSACTION_CORRUPT');
|
|
169
|
+
}
|
|
170
|
+
if (parsed.items.some((item) =>
|
|
171
|
+
!pathInside(base, item.oldPath)
|
|
172
|
+
|| !pathInside(base, item.nextPath)
|
|
173
|
+
|| !pathInside(transactionRoot(base), item.stagePath))) {
|
|
174
|
+
throw new Error('MEMORY_TRANSACTION_PATH_INVALID');
|
|
175
|
+
}
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function rollForwardTransactionUnlocked(base: string): Promise<void> {
|
|
180
|
+
const manifest = await readTransactionManifest(base);
|
|
181
|
+
if (!manifest) return;
|
|
182
|
+
for (const item of manifest.items) {
|
|
183
|
+
const targetRaw = await readFile(item.nextPath, 'utf8').catch(() => null);
|
|
184
|
+
if (targetRaw !== null && sha256(targetRaw) === item.targetVersion) {
|
|
185
|
+
if (item.oldPath !== item.nextPath) await unlink(item.oldPath).catch(() => {});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (item.nextPath !== item.oldPath && targetRaw !== null) {
|
|
189
|
+
throw new Error('MEMORY_TRANSACTION_CONFLICT');
|
|
190
|
+
}
|
|
191
|
+
const oldRaw = await readFile(item.oldPath, 'utf8').catch(() => null);
|
|
192
|
+
if (oldRaw === null || sha256(oldRaw) !== item.expectedVersion) {
|
|
193
|
+
throw new Error('MEMORY_TRANSACTION_CONFLICT');
|
|
194
|
+
}
|
|
195
|
+
await mkdir(dirname(item.nextPath), { recursive: true, mode: 0o700 });
|
|
196
|
+
markInternalRecordEvent(item.nextPath);
|
|
197
|
+
if (item.oldPath !== item.nextPath) markInternalRecordEvent(item.oldPath);
|
|
198
|
+
await rename(item.stagePath, item.nextPath);
|
|
199
|
+
if (item.oldPath !== item.nextPath) await unlink(item.oldPath).catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
await atomicWriteFile(generationPath(base), `${manifest.id}\n`);
|
|
202
|
+
await unlink(transactionManifestPath(base)).catch(() => {});
|
|
203
|
+
await rm(join(transactionRoot(base), manifest.id), { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const TRANSACTION_POISON_ERRORS = new Set([
|
|
207
|
+
'MEMORY_TRANSACTION_CORRUPT',
|
|
208
|
+
'MEMORY_TRANSACTION_PATH_INVALID',
|
|
209
|
+
'MEMORY_TRANSACTION_CONFLICT',
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
interface ScopeRecovery {
|
|
213
|
+
recovered: boolean;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface QuarantineMetadata {
|
|
217
|
+
schemaVersion: 1;
|
|
218
|
+
quarantinedAt: number;
|
|
219
|
+
reason: string;
|
|
220
|
+
originalManifestPath: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function quarantineDiagnosticsFor(base: string): Promise<MemoryDiagnostic[]> {
|
|
224
|
+
const root = transactionRoot(base);
|
|
225
|
+
let names: string[];
|
|
226
|
+
try {
|
|
227
|
+
names = (await readdir(root, { withFileTypes: true }))
|
|
228
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('quarantine-'))
|
|
229
|
+
.map((entry) => entry.name);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
const diagnostics: MemoryDiagnostic[] = [];
|
|
235
|
+
for (const name of names.sort()) {
|
|
236
|
+
const target = join(root, name);
|
|
237
|
+
let metadata: QuarantineMetadata | null = null;
|
|
238
|
+
try {
|
|
239
|
+
metadata = JSON.parse(await readFile(join(target, 'diagnostic.json'), 'utf8')) as QuarantineMetadata;
|
|
240
|
+
} catch {
|
|
241
|
+
// 旧版本隔离目录没有元数据时仍需告警,不能因升级让诊断沉默。
|
|
242
|
+
}
|
|
243
|
+
const reason = metadata?.reason ?? 'MEMORY_TRANSACTION_QUARANTINED';
|
|
244
|
+
diagnostics.push({
|
|
245
|
+
path: metadata?.originalManifestPath ?? join(target, 'current.json'),
|
|
246
|
+
message:
|
|
247
|
+
`记忆事务无法前滚(${reason}),已隔离到 ${target} 并继续读取现有记录;`
|
|
248
|
+
+ '该批次的改动可能只应用了一部分,请复核相关记忆。',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return diagnostics;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function clearScopeTransactionQuarantine(
|
|
255
|
+
scope: MemoryScope,
|
|
256
|
+
workspaceId?: string,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
const root = transactionRoot(scopeDirectory(scope, workspaceId));
|
|
259
|
+
let names: string[] = [];
|
|
260
|
+
try {
|
|
261
|
+
names = (await readdir(root, { withFileTypes: true }))
|
|
262
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('quarantine-'))
|
|
263
|
+
.map((entry) => entry.name);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
266
|
+
}
|
|
267
|
+
await Promise.all(names.map(async (name) => {
|
|
268
|
+
const quarantine = join(root, name);
|
|
269
|
+
try {
|
|
270
|
+
const manifest = JSON.parse(await readFile(join(quarantine, 'current.json'), 'utf8')) as { id?: unknown };
|
|
271
|
+
if (typeof manifest.id === 'string' && /^[a-f0-9-]{16,}$/i.test(manifest.id)) {
|
|
272
|
+
await rm(join(root, manifest.id), { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
// 损坏清单本来就可能无法解析;清理隔离目录仍继续。
|
|
276
|
+
}
|
|
277
|
+
await rm(quarantine, { recursive: true, force: true });
|
|
278
|
+
}));
|
|
279
|
+
invalidateScope(scope, workspaceId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 隔离操作的专用锁路径(实际锁文件是 `<它>.lock`)。 */
|
|
283
|
+
function quarantineLockPath(base: string): string {
|
|
284
|
+
return join(transactionRoot(base), '.quarantine');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 把无法前滚的事务清单挪进 `.transactions/quarantine-<ts>/`,让读路径继续可用。
|
|
289
|
+
*
|
|
290
|
+
* ⚠️ 这是**读路径**独有的策略;写路径(replaceMemoryBatchVersioned)仍然照常抛错——
|
|
291
|
+
* 写入方必须看到冲突,不能替它把别人已提交的事务悄悄扔掉。
|
|
292
|
+
*
|
|
293
|
+
* ⚠️ **必须持 `.quarantine` 锁调用**(见 quarantineTransaction)。它是
|
|
294
|
+
* `mkdir + rename + unlink + 写元数据` 的多步变更,无锁并发会各建一个隔离目录。
|
|
295
|
+
*/
|
|
296
|
+
async function quarantineTransactionLocked(base: string, reason: string): Promise<void> {
|
|
297
|
+
const manifestPath = transactionManifestPath(base);
|
|
298
|
+
const manifestRaw = await readFile(manifestPath, 'utf8').catch(() => null);
|
|
299
|
+
// 拿到锁后清单已经不在 = 另一个读者/进程刚刚隔离过同一份。直接返回:
|
|
300
|
+
// 不重复建目录、不重复告警。这一条取代了以前那个只覆盖 ENOENT 交错的特判。
|
|
301
|
+
if (manifestRaw === null) return;
|
|
302
|
+
|
|
303
|
+
const target = join(
|
|
304
|
+
transactionRoot(base),
|
|
305
|
+
`quarantine-${Date.now()}-${randomUUID().slice(0, 8)}`,
|
|
306
|
+
);
|
|
307
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
308
|
+
let claimed = true;
|
|
309
|
+
try {
|
|
310
|
+
await rename(manifestPath, join(target, 'current.json'));
|
|
311
|
+
} catch {
|
|
312
|
+
// 跨卷或某些网络盘上 rename 可能失败。必须先保存并校验副本,再移除原清单;
|
|
313
|
+
// 不能为了让读路径继续就丢掉唯一的恢复证据。
|
|
314
|
+
const preserved = join(target, 'current.json');
|
|
315
|
+
await atomicWriteFile(preserved, manifestRaw).catch(() => undefined);
|
|
316
|
+
const verified = await readFile(preserved, 'utf8')
|
|
317
|
+
.then((value) => sha256(value) === sha256(manifestRaw))
|
|
318
|
+
.catch(() => false);
|
|
319
|
+
claimed = verified
|
|
320
|
+
? await unlink(manifestPath).then(() => true).catch(() => false)
|
|
321
|
+
: false;
|
|
322
|
+
}
|
|
323
|
+
if (!claimed) {
|
|
324
|
+
// 证据没保住就别留下空目录——它会被 quarantineDiagnosticsFor 当成一次真实隔离,
|
|
325
|
+
// 报出「已隔离到 XXX」却什么都恢复不了。
|
|
326
|
+
await rm(target, { recursive: true, force: true }).catch(() => undefined);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const metadata: QuarantineMetadata = {
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
quarantinedAt: Date.now(),
|
|
332
|
+
reason,
|
|
333
|
+
originalManifestPath: manifestPath,
|
|
334
|
+
};
|
|
335
|
+
await atomicWriteFile(join(target, 'diagnostic.json'), `${JSON.stringify(metadata, null, 2)}\n`)
|
|
336
|
+
.catch(() => undefined);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 加锁版入口。用**独立**的 `.quarantine` 锁而不是 `.records`:
|
|
341
|
+
* 需要互斥的只有「另一个隔离尝试」——合法的在飞事务其清单由 atomicWriteFile 写出,
|
|
342
|
+
* 永远是完整可解析的,不会被误隔离,所以无需与记录写入互斥。
|
|
343
|
+
* 而且 `memoryService.remember()` 是**持着 `.records` 锁**调用 scanMemoryScope 的,
|
|
344
|
+
* 这里若复用 `.records` 会自锁(文件锁不可重入)直接等到超时。
|
|
345
|
+
*/
|
|
346
|
+
async function quarantineTransaction(base: string, reason: string): Promise<void> {
|
|
347
|
+
const release = await acquireFileLock(quarantineLockPath(base));
|
|
348
|
+
try {
|
|
349
|
+
await quarantineTransactionLocked(base, reason);
|
|
350
|
+
} finally {
|
|
351
|
+
await release();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function recoverScopeTransaction(base: string): Promise<ScopeRecovery> {
|
|
356
|
+
let manifestExists: boolean;
|
|
357
|
+
try {
|
|
358
|
+
manifestExists = (await readTransactionManifest(base)) !== null;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const reason = (error as Error).message;
|
|
361
|
+
if (!TRANSACTION_POISON_ERRORS.has(reason)) throw error;
|
|
362
|
+
// 清单读不出来(结构损坏 / JSON 语法错 / 路径越界)→ 隔离。
|
|
363
|
+
// ⚠️ 这里曾写着「锁都不必拿,直接隔离」并真的裸调 —— 那是错的:隔离是多步文件变更,
|
|
364
|
+
// 两个并发读者(典型触发者:一次 remember() 后台刷新视图的扫描 + 用户这次读)
|
|
365
|
+
// 会各建一个隔离目录,其中一个还留不下 current.json,却照样报「已隔离到 XXX」。
|
|
366
|
+
// 同一函数在下面 roll-forward 失败处本来就是持锁调用的,这个不对称就是缺陷本身。
|
|
367
|
+
await quarantineTransaction(base, reason);
|
|
368
|
+
return { recovered: true };
|
|
369
|
+
}
|
|
370
|
+
if (!manifestExists) return { recovered: false };
|
|
371
|
+
const release = await acquireFileLock(join(base, '.records'));
|
|
372
|
+
try {
|
|
373
|
+
await rollForwardTransactionUnlocked(base);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const reason = (error as Error).message;
|
|
376
|
+
if (!TRANSACTION_POISON_ERRORS.has(reason)) throw error;
|
|
377
|
+
await quarantineTransaction(base, reason);
|
|
378
|
+
return { recovered: true };
|
|
379
|
+
} finally {
|
|
380
|
+
await release();
|
|
381
|
+
}
|
|
382
|
+
return { recovered: true };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function scopeKey(scope: MemoryScope, workspaceId?: string): string {
|
|
386
|
+
return scopeDirectory(scope, workspaceId);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 重建 `views/topics.md`(人类可读视图,不是权威数据)。
|
|
391
|
+
*
|
|
392
|
+
* 只在两种时刻调用:**全量扫描之后**,以及**记忆变更之后**。读路径不再触发它——
|
|
393
|
+
* 否则每次 recall / 建上下文包都要 readFile 一次,连「缓存命中」都带磁盘 IO,
|
|
394
|
+
* 而且 read-only 会话也会写盘。
|
|
395
|
+
*/
|
|
396
|
+
export async function refreshTopicView(
|
|
397
|
+
scope: MemoryScope,
|
|
398
|
+
workspaceId?: string,
|
|
399
|
+
): Promise<void> {
|
|
400
|
+
const snapshot = await scanMemoryScope(scope, workspaceId);
|
|
401
|
+
await ensureTopicView(snapshot, scope, workspaceId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function currentEpoch(key: string): number {
|
|
405
|
+
return scopeState(key).epoch;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function invalidateScope(scope: MemoryScope, workspaceId?: string): void {
|
|
409
|
+
const key = scopeKey(scope, workspaceId);
|
|
410
|
+
const state = scopeState(key);
|
|
411
|
+
state.epoch += 1;
|
|
412
|
+
if (state.cache && !state.cache.dirty) {
|
|
413
|
+
state.cache.dirty = true;
|
|
414
|
+
repositoryDiagnostics.watchInvalidations += 1;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function closeScopeWatchers(state: ScopeRuntimeState): void {
|
|
419
|
+
for (const watcher of state.watchers) watcher.close();
|
|
420
|
+
state.watchers = [];
|
|
421
|
+
state.watcherComplete = false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function ensureScopeWatcher(
|
|
425
|
+
base: string,
|
|
426
|
+
scope: MemoryScope,
|
|
427
|
+
workspaceId?: string,
|
|
428
|
+
): boolean {
|
|
429
|
+
const key = scopeKey(scope, workspaceId);
|
|
430
|
+
const state = scopeState(key);
|
|
431
|
+
if (state.watchers.length && !state.watcherRefreshRequired) return state.watcherComplete;
|
|
432
|
+
closeScopeWatchers(state);
|
|
433
|
+
state.watcherRefreshRequired = false;
|
|
434
|
+
try {
|
|
435
|
+
const watchers: FSWatcher[] = [];
|
|
436
|
+
const rootWatcher = watch(base, (event, filename) => {
|
|
437
|
+
if (typeof filename !== 'string') {
|
|
438
|
+
// 根目录的无名事件无法判定来源,不直接打脏;让下次读立即做轻量审计。
|
|
439
|
+
if (state.cache) state.cache.lastAuditAt = 0;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (GENERATED_SCOPE_ENTRIES.has(filename)) return;
|
|
443
|
+
if (event !== 'rename' || !CATEGORY_SCOPE_ENTRIES.has(filename)) return;
|
|
444
|
+
state.watcherRefreshRequired = true;
|
|
445
|
+
invalidateScope(scope, workspaceId);
|
|
446
|
+
});
|
|
447
|
+
rootWatcher.unref();
|
|
448
|
+
watchers.push(rootWatcher);
|
|
449
|
+
|
|
450
|
+
const watchCategory = (path: string): void => {
|
|
451
|
+
const watcher = watch(path, (_event, filename) => {
|
|
452
|
+
if (typeof filename === 'string' && !filename.endsWith('.md')) return;
|
|
453
|
+
if (typeof filename === 'string' && consumeInternalRecordEvent(join(path, filename))) return;
|
|
454
|
+
// 全量扫描刚结束时,部分平台会延迟送达「已被该快照包含」的旧事件。
|
|
455
|
+
// 这个短窗口内不盲目打脏,而是强制下次读做版本/目录审计;因此真实外部
|
|
456
|
+
// 修改仍会被发现,而迟到的旧事件不会导致无意义全量重扫。
|
|
457
|
+
if (Date.now() < state.suppressDirtyUntil) {
|
|
458
|
+
if (state.cache) state.cache.lastAuditAt = 0;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
invalidateScope(scope, workspaceId);
|
|
462
|
+
});
|
|
463
|
+
watcher.unref();
|
|
464
|
+
watchers.push(watcher);
|
|
465
|
+
};
|
|
466
|
+
// ★ 覆盖必须**完整**才敢自称 watcherActive,否则读路径不能享受 1 秒信任窗(见下)。
|
|
467
|
+
// 实测(Windows,`fs.watch` 非递归):手工编辑 `<base>/profile/x.md` 只触发**分类目录**
|
|
468
|
+
// 上的 watcher(`change x.md`),base 上的 watcher 一声不吭——所以分类目录的 watcher
|
|
469
|
+
// 是「改动能不能被发现」的唯一依据。
|
|
470
|
+
// ⚠️ 「不存在的分类目录」不算覆盖缺口:它装不下任何记录,而且它**被创建**时 base 上的
|
|
471
|
+
// watcher 会收到 `rename <category>`(同样实测过),照样能作废缓存。只有目录确实存在
|
|
472
|
+
// 却挂不上 watcher(句柄耗尽、权限、网络盘…)才是真缺口,那时必须每次读都做审计。
|
|
473
|
+
let complete = true;
|
|
474
|
+
for (const category of ALL_CATEGORIES) {
|
|
475
|
+
try {
|
|
476
|
+
watchCategory(join(base, CATEGORY_DIR[category]));
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') complete = false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const fallback = () => {
|
|
482
|
+
closeScopeWatchers(state);
|
|
483
|
+
state.watcherRefreshRequired = true;
|
|
484
|
+
if (state.cache) watcherActiveFallback(state.cache);
|
|
485
|
+
};
|
|
486
|
+
for (const watcher of watchers) watcher.on('error', fallback);
|
|
487
|
+
state.watchers = watchers;
|
|
488
|
+
state.watcherComplete = complete;
|
|
489
|
+
return complete;
|
|
490
|
+
} catch {
|
|
491
|
+
closeScopeWatchers(state);
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function watcherActiveFallback(cached: ScopeCacheEntry): void {
|
|
497
|
+
cached.dirty = true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function evictScopeCaches(): void {
|
|
501
|
+
if (scopeStates.size <= MAX_SCOPE_CACHE_ENTRIES) return;
|
|
502
|
+
const victims = [...scopeStates.entries()]
|
|
503
|
+
.sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt)
|
|
504
|
+
.slice(0, scopeStates.size - MAX_SCOPE_CACHE_ENTRIES);
|
|
505
|
+
for (const [key] of victims) {
|
|
506
|
+
const state = scopeStates.get(key);
|
|
507
|
+
if (state) closeScopeWatchers(state);
|
|
508
|
+
scopeStates.delete(key);
|
|
509
|
+
// ⚠️ 不能写 `path.startsWith(`${key}/`)`:Windows 上分隔符是 `\`,该条件恒为 false,
|
|
510
|
+
// 被淘汰 scope 的文件缓存永远清不掉 → fileCache 无上限增长。
|
|
511
|
+
for (const path of fileCache.keys()) {
|
|
512
|
+
if (pathContains(key, path)) fileCache.delete(path);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function settleRecentWatcherEvents(state: ScopeRuntimeState): Promise<boolean> {
|
|
518
|
+
await new Promise<void>((resolvePromise) => { setImmediate(resolvePromise); });
|
|
519
|
+
if (state.cache?.dirty) return false;
|
|
520
|
+
const now = Date.now();
|
|
521
|
+
if (now - state.lastDrainAt >= WATCHER_SETTLE_REUSE_MS) {
|
|
522
|
+
await new Promise<void>((resolvePromise) => {
|
|
523
|
+
setTimeout(resolvePromise, WATCHER_SETTLE_WAIT_MS);
|
|
524
|
+
});
|
|
525
|
+
state.lastDrainAt = Date.now();
|
|
526
|
+
}
|
|
527
|
+
// 正常 watcher 在上面的 settle 窗口中会直接打脏;扫描结束后的抑制窗口只把
|
|
528
|
+
// lastAuditAt 清零,故仅这一种情况需要逐文件核验。稳定缓存不做 stat 扫描。
|
|
529
|
+
return state.cache?.lastAuditAt === 0 || Date.now() < state.suppressDirtyUntil;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function cachedFilesUnchanged(snapshot: MemorySnapshot): Promise<boolean> {
|
|
533
|
+
const checks = await Promise.all(snapshot.records.map(async (record) => {
|
|
534
|
+
const path = memoryRecordPath(record);
|
|
535
|
+
const cached = fileCache.get(path);
|
|
536
|
+
if (!cached) return false;
|
|
537
|
+
try {
|
|
538
|
+
const info = await stat(path);
|
|
539
|
+
return cached.mtimeMs === info.mtimeMs && cached.size === info.size;
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}));
|
|
544
|
+
return checks.every(Boolean);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function directoryFingerprint(base: string): Promise<string> {
|
|
548
|
+
const values = await Promise.all(ALL_CATEGORIES.map(async (category) => {
|
|
549
|
+
const dir = join(base, CATEGORY_DIR[category]);
|
|
550
|
+
try {
|
|
551
|
+
const info = await stat(dir);
|
|
552
|
+
return `${category}:${info.mtimeMs}:${info.size}`;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return `${category}:missing`;
|
|
555
|
+
return `${category}:error`;
|
|
556
|
+
}
|
|
557
|
+
}));
|
|
558
|
+
return values.join('|');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function readCached(path: string): Promise<CachedFile> {
|
|
562
|
+
const info = await stat(path);
|
|
563
|
+
const cached = fileCache.get(path);
|
|
564
|
+
if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size)
|
|
565
|
+
return cached;
|
|
566
|
+
const raw = await readFile(path, 'utf8');
|
|
567
|
+
const version = sha256(raw);
|
|
568
|
+
try {
|
|
569
|
+
const next: CachedFile = {
|
|
570
|
+
mtimeMs: info.mtimeMs,
|
|
571
|
+
size: info.size,
|
|
572
|
+
version,
|
|
573
|
+
record: parseMemoryRecord(raw, path),
|
|
574
|
+
};
|
|
575
|
+
fileCache.set(path, next);
|
|
576
|
+
return next;
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const next: CachedFile = {
|
|
579
|
+
mtimeMs: info.mtimeMs,
|
|
580
|
+
size: info.size,
|
|
581
|
+
version,
|
|
582
|
+
diagnostic: { path, message: (error as Error).message },
|
|
583
|
+
};
|
|
584
|
+
fileCache.set(path, next);
|
|
585
|
+
return next;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export async function scanMemoryScope(
|
|
590
|
+
scope: MemoryScope,
|
|
591
|
+
workspaceId?: string,
|
|
592
|
+
attempt = 0,
|
|
593
|
+
): Promise<MemorySnapshot> {
|
|
594
|
+
const base = scopeDirectory(scope, workspaceId);
|
|
595
|
+
const key = scopeKey(scope, workspaceId);
|
|
596
|
+
const state = scopeState(key);
|
|
597
|
+
const recovery = await recoverScopeTransaction(base);
|
|
598
|
+
let cachedScope = state.cache;
|
|
599
|
+
if (recovery.recovered && cachedScope) cachedScope.dirty = true;
|
|
600
|
+
const generationBefore = await readGeneration(base);
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
// ★ 读路径保持纯读:缓存命中直接返回快照,不再顺手 readFile / 写 views/topics.md。
|
|
603
|
+
// 旧实现把 ensureTopicView 挂在**所有**返回分支上,于是「缓存命中」仍有磁盘 IO,
|
|
604
|
+
// 而且 memoryAccess:'read-only' 的会话也会写盘——仓储的读 API 不该有副作用。
|
|
605
|
+
// 视图改由「全量扫描后」与「变更后」触发(见 refreshTopicView)。
|
|
606
|
+
// ★ 新鲜度两条腿,缺一不可:
|
|
607
|
+
//
|
|
608
|
+
// ① **排空一次事件循环**再判 dirty。`fs.watch` 的回调是排队派发的,「watcher 已挂上」
|
|
609
|
+
// 不等于「它已经把刚才那次改动告诉我了」。实测(Windows):改完文件后只要让出
|
|
610
|
+
// 一个 setImmediate tick,事件就已送达。这一步零 IO,却把「刚改完立刻读」这个
|
|
611
|
+
// 最常见的竞态消掉了。旧代码也想干这事,但把它锁在「距上次使用 <50ms」的条件里,
|
|
612
|
+
// 两次读之间只要多做点别的(哪怕只是写一次设置)就绕过去了。
|
|
613
|
+
//
|
|
614
|
+
// ② watcher 覆盖不全或压根没触发时,用 stat 审计兜底,节流到每秒最多一次。
|
|
615
|
+
// 审计是 O(记录数) 次 stat——实测 300 条记录约 40ms,放在每次读上太贵
|
|
616
|
+
// (每回合 recall + buildContextBundle 各一次),所以必须节流;但 30 秒的信任窗
|
|
617
|
+
// 又太松,手编要等半分钟才生效。1 秒是两边都能接受的点。
|
|
618
|
+
//
|
|
619
|
+
// ⚠️ 别再让读路径去写 `views/topics.md` 来「顺便」作废缓存——那是以前手编能被发现的
|
|
620
|
+
// 真实原因,纯属巧合,代价是每次读都做磁盘 IO。
|
|
621
|
+
let verifyAfterSettle = false;
|
|
622
|
+
if (cachedScope && !cachedScope.dirty) {
|
|
623
|
+
verifyAfterSettle = await settleRecentWatcherEvents(state);
|
|
624
|
+
cachedScope = state.cache;
|
|
625
|
+
}
|
|
626
|
+
if (cachedScope && !cachedScope.dirty && verifyAfterSettle) {
|
|
627
|
+
repositoryDiagnostics.fallbackAudits += 1;
|
|
628
|
+
if (
|
|
629
|
+
!await cachedFilesUnchanged(cachedScope.snapshot)
|
|
630
|
+
|| cachedScope.directoryFingerprint !== await directoryFingerprint(base)
|
|
631
|
+
) {
|
|
632
|
+
cachedScope.dirty = true;
|
|
633
|
+
} else {
|
|
634
|
+
cachedScope.lastAuditAt = Date.now();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (cachedScope && !cachedScope.dirty) {
|
|
638
|
+
state.lastUsedAt = now;
|
|
639
|
+
// 只有 watcher 覆盖完整时才敢跳过审计——覆盖有缺口就等于没人会告诉我们变更,
|
|
640
|
+
// 那个 1 秒信任窗就失去依据,必须每次读都审计。
|
|
641
|
+
if (state.watcherComplete && now - cachedScope.lastAuditAt < SCOPE_AUDIT_TTL_MS) {
|
|
642
|
+
repositoryDiagnostics.snapshotHits += 1;
|
|
643
|
+
return cachedScope.snapshot;
|
|
644
|
+
}
|
|
645
|
+
repositoryDiagnostics.fallbackAudits += 1;
|
|
646
|
+
if (
|
|
647
|
+
await cachedFilesUnchanged(cachedScope.snapshot)
|
|
648
|
+
&& cachedScope.directoryFingerprint === await directoryFingerprint(base)
|
|
649
|
+
) {
|
|
650
|
+
cachedScope.lastAuditAt = now;
|
|
651
|
+
repositoryDiagnostics.snapshotHits += 1;
|
|
652
|
+
return cachedScope.snapshot;
|
|
653
|
+
}
|
|
654
|
+
cachedScope.dirty = true;
|
|
655
|
+
}
|
|
656
|
+
repositoryDiagnostics.fullScans += 1;
|
|
657
|
+
// ★ 先挂 watcher 再读文件,并记下代际号。
|
|
658
|
+
// 旧顺序是「先把记录全读完,最后才挂 watcher」,中间那段窗口里发生的改动**两头都漏**:
|
|
659
|
+
// 读的时候还没改,watcher 又还没挂上——扫完却把这份已经过时的快照标记为干净缓存,
|
|
660
|
+
// 于是直到下一次兜底审计(现在是 1 秒)之前,谁都发现不了。这是原设计里的 TOCTOU。
|
|
661
|
+
// 现在:先挂 watcher,扫完再比对代际号,期间有任何改动就重扫。
|
|
662
|
+
const watcherActive = ensureScopeWatcher(base, scope, workspaceId);
|
|
663
|
+
const epochBefore = currentEpoch(key);
|
|
664
|
+
const records: MemoryRecordV2[] = [];
|
|
665
|
+
// 隔离事务留下的告警要跟着本次快照走完,不能只写日志然后消失——
|
|
666
|
+
// diagnostics 是既有的、Desktop 与 diagnosticSuffix() 都会展示的通道。
|
|
667
|
+
const diagnostics: MemoryDiagnostic[] = await quarantineDiagnosticsFor(base);
|
|
668
|
+
const versions: Record<string, MemoryVersion> = {};
|
|
669
|
+
const seen = new Set<string>();
|
|
670
|
+
for (const category of ALL_CATEGORIES) {
|
|
671
|
+
const dir = join(base, CATEGORY_DIR[category]);
|
|
672
|
+
let names: string[] = [];
|
|
673
|
+
try {
|
|
674
|
+
names = (await readdir(dir, { withFileTypes: true }))
|
|
675
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
676
|
+
.map((entry) => entry.name);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
679
|
+
diagnostics.push({ path: dir, message: (error as Error).message });
|
|
680
|
+
}
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
for (const name of names) {
|
|
684
|
+
const path = join(dir, name);
|
|
685
|
+
seen.add(path);
|
|
686
|
+
try {
|
|
687
|
+
const cached = await readCached(path);
|
|
688
|
+
if (cached.diagnostic) {
|
|
689
|
+
diagnostics.push(cached.diagnostic);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const record = cached.record!;
|
|
693
|
+
if (
|
|
694
|
+
record.scope !== scope ||
|
|
695
|
+
record.category !== category ||
|
|
696
|
+
record.workspaceId !==
|
|
697
|
+
(scope === 'workspace' ? workspaceId : undefined) ||
|
|
698
|
+
name !== `${record.id}.md`
|
|
699
|
+
) {
|
|
700
|
+
diagnostics.push({ path, message: '记忆元数据与所在目录不一致' });
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
records.push(record);
|
|
704
|
+
versions[record.id] = cached.version;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
diagnostics.push({ path, message: (error as Error).message });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// 同上:硬编码 `/` 在 Windows 上永不命中,已删除记录的缓存条目会永久滞留。
|
|
712
|
+
for (const path of fileCache.keys()) {
|
|
713
|
+
if (path.endsWith('.md') && !seen.has(path) && pathContains(base, path)) {
|
|
714
|
+
fileCache.delete(path);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
records.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
718
|
+
const snapshot = { records, versions, diagnostics, createdAt: Date.now() };
|
|
719
|
+
const tailRecovery = await recoverScopeTransaction(base);
|
|
720
|
+
if (
|
|
721
|
+
(tailRecovery.recovered || generationBefore !== await readGeneration(base))
|
|
722
|
+
&& attempt < MAX_SCAN_RETRIES
|
|
723
|
+
) {
|
|
724
|
+
invalidateScope(scope, workspaceId);
|
|
725
|
+
return scanMemoryScope(scope, workspaceId, attempt + 1);
|
|
726
|
+
}
|
|
727
|
+
// 让排队中的 watcher 回调先送达,再判断这次扫描期间有没有人改过东西。
|
|
728
|
+
await new Promise<void>((resolvePromise) => { setImmediate(resolvePromise); });
|
|
729
|
+
// ⚠️ 重试必须有上限:持续写入的场景(cron 批量、另一个宿主导入、迁移)下每次重扫都会
|
|
730
|
+
// 看到新的代际号,无上限就是**活锁——读永远不返回**(还每轮重开 7 个 watcher)。
|
|
731
|
+
// 撞顶就照常缓存,让 1 秒兜底审计去纠正:最坏是几毫秒的陈旧快照且会自愈,
|
|
732
|
+
// 远好过一次挂死的 recall。
|
|
733
|
+
const changedDuringScan = currentEpoch(key) !== epochBefore;
|
|
734
|
+
if (changedDuringScan && attempt < MAX_SCAN_RETRIES) {
|
|
735
|
+
return scanMemoryScope(scope, workspaceId, attempt + 1);
|
|
736
|
+
}
|
|
737
|
+
state.cache = {
|
|
738
|
+
snapshot,
|
|
739
|
+
dirty: false,
|
|
740
|
+
directoryFingerprint: await directoryFingerprint(base),
|
|
741
|
+
// 撞了重试上限才落库的快照是**已知可能陈旧**的,不能再送它一个完整的 1 秒信任窗——
|
|
742
|
+
// 把审计时间戳清零,下一次读立刻走 stat 审计纠正。
|
|
743
|
+
lastAuditAt: changedDuringScan ? 0 : Date.now(),
|
|
744
|
+
};
|
|
745
|
+
state.watcherComplete = watcherActive;
|
|
746
|
+
state.lastUsedAt = Date.now();
|
|
747
|
+
state.suppressDirtyUntil = Date.now() + 100;
|
|
748
|
+
evictScopeCaches();
|
|
749
|
+
return snapshot;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export async function readVersionedMemory(
|
|
753
|
+
id: string,
|
|
754
|
+
scope: MemoryScope,
|
|
755
|
+
workspaceId?: string,
|
|
756
|
+
): Promise<VersionedMemoryRecord | null> {
|
|
757
|
+
const snapshot = await scanMemoryScope(scope, workspaceId);
|
|
758
|
+
const record = snapshot.records.find((item) => item.id === id);
|
|
759
|
+
return record ? { record, version: snapshot.versions[id]! } : null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export async function writeNewMemory(
|
|
763
|
+
record: MemoryRecordV2,
|
|
764
|
+
): Promise<MemoryVersion> {
|
|
765
|
+
const path = memoryRecordPath(record);
|
|
766
|
+
const content = serializeMemoryRecord(record);
|
|
767
|
+
markInternalRecordEvent(path);
|
|
768
|
+
await atomicWriteFile(path, content);
|
|
769
|
+
fileCache.delete(path);
|
|
770
|
+
invalidateScope(record.scope, record.workspaceId);
|
|
771
|
+
return sha256(content);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export interface VersionedMemoryReplacement {
|
|
775
|
+
current: VersionedMemoryRecord;
|
|
776
|
+
next: MemoryRecordV2;
|
|
777
|
+
expectedVersion: MemoryVersion;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Recoverable, cross-process batch replacement for one memory scope. */
|
|
781
|
+
export async function replaceMemoryBatchVersioned(
|
|
782
|
+
replacements: readonly VersionedMemoryReplacement[],
|
|
783
|
+
): Promise<Record<string, MemoryVersion>> {
|
|
784
|
+
if (replacements.length === 0) return {};
|
|
785
|
+
const first = replacements[0]!;
|
|
786
|
+
const base = scopeDirectory(first.next.scope, first.next.workspaceId);
|
|
787
|
+
if (replacements.some(({ current, next }) =>
|
|
788
|
+
next.scope !== first.next.scope
|
|
789
|
+
|| next.workspaceId !== first.next.workspaceId
|
|
790
|
+
|| current.record.scope !== first.next.scope
|
|
791
|
+
|| current.record.workspaceId !== first.next.workspaceId)) {
|
|
792
|
+
throw new Error('MEMORY_BATCH_SCOPE_MISMATCH');
|
|
793
|
+
}
|
|
794
|
+
const release = await acquireFileLock(join(base, '.records'));
|
|
795
|
+
const transactionId = randomUUID();
|
|
796
|
+
const stageDir = join(transactionRoot(base), transactionId);
|
|
797
|
+
let prepared = false;
|
|
798
|
+
try {
|
|
799
|
+
await rollForwardTransactionUnlocked(base);
|
|
800
|
+
const items: MemoryTransactionItem[] = [];
|
|
801
|
+
const versions: Record<string, MemoryVersion> = {};
|
|
802
|
+
await mkdir(stageDir, { recursive: true, mode: 0o700 });
|
|
803
|
+
for (const replacement of replacements) {
|
|
804
|
+
const oldPath = memoryRecordPath(replacement.current.record);
|
|
805
|
+
const nextPath = memoryRecordPath(replacement.next);
|
|
806
|
+
const liveRaw = await readFile(oldPath, 'utf8').catch((error) => {
|
|
807
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
808
|
+
throw error;
|
|
809
|
+
});
|
|
810
|
+
if (liveRaw === null) throw new Error('MEMORY_NOT_FOUND');
|
|
811
|
+
if (sha256(liveRaw) !== replacement.expectedVersion) throw new Error('MEMORY_VERSION_CONFLICT');
|
|
812
|
+
const content = serializeMemoryRecord(replacement.next);
|
|
813
|
+
const targetVersion = sha256(content);
|
|
814
|
+
const stagePath = join(stageDir, `${replacement.next.id}.md`);
|
|
815
|
+
await atomicWriteFile(stagePath, content);
|
|
816
|
+
items.push({
|
|
817
|
+
id: replacement.next.id,
|
|
818
|
+
oldPath,
|
|
819
|
+
nextPath,
|
|
820
|
+
stagePath,
|
|
821
|
+
expectedVersion: replacement.expectedVersion,
|
|
822
|
+
targetVersion,
|
|
823
|
+
});
|
|
824
|
+
versions[replacement.next.id] = targetVersion;
|
|
825
|
+
}
|
|
826
|
+
const manifest: MemoryTransactionManifest = {
|
|
827
|
+
schemaVersion: 1,
|
|
828
|
+
id: transactionId,
|
|
829
|
+
items,
|
|
830
|
+
};
|
|
831
|
+
await atomicWriteFile(transactionManifestPath(base), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
832
|
+
prepared = true;
|
|
833
|
+
await rollForwardTransactionUnlocked(base);
|
|
834
|
+
for (const replacement of replacements) {
|
|
835
|
+
fileCache.delete(memoryRecordPath(replacement.current.record));
|
|
836
|
+
fileCache.delete(memoryRecordPath(replacement.next));
|
|
837
|
+
}
|
|
838
|
+
invalidateScope(first.next.scope, first.next.workspaceId);
|
|
839
|
+
return versions;
|
|
840
|
+
} finally {
|
|
841
|
+
if (!prepared) await rm(stageDir, { recursive: true, force: true });
|
|
842
|
+
await release();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export async function replaceVersionedMemory(
|
|
847
|
+
current: VersionedMemoryRecord,
|
|
848
|
+
next: MemoryRecordV2,
|
|
849
|
+
expectedVersion: MemoryVersion,
|
|
850
|
+
): Promise<MemoryVersion> {
|
|
851
|
+
const versions = await replaceMemoryBatchVersioned([{ current, next, expectedVersion }]);
|
|
852
|
+
return versions[next.id]!;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export async function removeVersionedMemory(
|
|
856
|
+
current: VersionedMemoryRecord,
|
|
857
|
+
expectedVersion?: MemoryVersion,
|
|
858
|
+
): Promise<boolean> {
|
|
859
|
+
const path = memoryRecordPath(current.record);
|
|
860
|
+
const release = await acquireFileLock(
|
|
861
|
+
join(
|
|
862
|
+
scopeDirectory(current.record.scope, current.record.workspaceId),
|
|
863
|
+
'.records',
|
|
864
|
+
),
|
|
865
|
+
);
|
|
866
|
+
try {
|
|
867
|
+
if (expectedVersion) {
|
|
868
|
+
const raw = await readFile(path, 'utf8').catch(() => null);
|
|
869
|
+
if (raw === null) return false;
|
|
870
|
+
if (sha256(raw) !== expectedVersion)
|
|
871
|
+
throw new Error('MEMORY_VERSION_CONFLICT');
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
markInternalRecordEvent(path);
|
|
875
|
+
await unlink(path);
|
|
876
|
+
fileCache.delete(path);
|
|
877
|
+
invalidateScope(current.record.scope, current.record.workspaceId);
|
|
878
|
+
return true;
|
|
879
|
+
} catch (error) {
|
|
880
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
881
|
+
throw error;
|
|
882
|
+
}
|
|
883
|
+
} finally {
|
|
884
|
+
await release();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function clearMemoryIndex(): void {
|
|
889
|
+
fileCache.clear();
|
|
890
|
+
internalRecordEvents.clear();
|
|
891
|
+
for (const state of scopeStates.values()) closeScopeWatchers(state);
|
|
892
|
+
scopeStates.clear();
|
|
893
|
+
}
|