@piagent/platform 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/README.md +10 -4
- package/README.vi.md +27 -0
- package/architecture/layers.json +96 -1
- package/docs/README.md +1 -0
- package/docs/architecture.md +1 -0
- package/docs/authority-manifest.md +1 -0
- package/docs/auto-delegation-policy.md +1 -0
- package/docs/capability-packs.md +1 -0
- package/docs/command-reference-vietnamese.md +5 -4
- package/docs/context-engine.md +1 -0
- package/docs/context-window-policy.md +1 -0
- package/docs/distribution-standard.md +8 -7
- package/docs/en/architecture.md +13 -0
- package/docs/herdr-workflow.md +1 -0
- package/docs/languages.json +20 -4
- package/docs/mcp-and-tools.md +125 -9
- package/docs/memory-policy.md +1 -0
- package/docs/model-options.md +1 -0
- package/docs/oauth-providers.md +1 -0
- package/docs/operator-manual-vietnamese.md +15 -12
- package/docs/package-architecture-notes.md +1 -0
- package/docs/project-adapters.md +1 -0
- package/docs/project-onboarding.md +1 -0
- package/docs/publishing-for-teams.md +37 -0
- package/docs/quality-benchmark.md +1 -0
- package/docs/quickstart-vietnamese.md +3 -2
- package/docs/release-install-policy.md +22 -8
- package/docs/runtime-harness-standard.md +2 -1
- package/docs/runtime-policy-design.md +1 -0
- package/docs/runtime-quality-baseline.md +1 -0
- package/docs/security-redaction-benchmark.md +1 -0
- package/docs/security-threat-model.md +2 -1
- package/docs/subagent-orchestration-capabilities.md +1 -0
- package/docs/subagents-and-multiagent.md +1 -0
- package/docs/task-implementation-contract.md +12 -4
- package/docs/task-lifecycle-vietnamese.md +1 -0
- package/docs/team-onboarding.md +3 -2
- package/docs/third-party-neutrality.md +26 -0
- package/docs/usage-observability.md +71 -0
- package/docs/vercel-docs-site.md +1 -0
- package/docs/vi/architecture.md +13 -0
- package/docs/workflow-recipes.md +1 -0
- package/docs-site/architecture.html +5 -1
- package/docs-site/benchmark.html +5 -1
- package/docs-site/commands.html +5 -1
- package/docs-site/content/en/index.html +49 -0
- package/docs-site/content/en/runtime.html +13 -0
- package/docs-site/content/index.html +49 -0
- package/docs-site/content/runtime.html +28 -0
- package/docs-site/context-engine.html +5 -1
- package/docs-site/en/architecture.html +5 -1
- package/docs-site/en/benchmark.html +5 -1
- package/docs-site/en/commands.html +5 -1
- package/docs-site/en/context-engine.html +5 -1
- package/docs-site/en/index.html +54 -1
- package/docs-site/en/mcp-approval.html +5 -1
- package/docs-site/en/mcp-auth.html +5 -1
- package/docs-site/en/mcp-servers.html +5 -1
- package/docs-site/en/mcp.html +5 -1
- package/docs-site/en/permissions.html +5 -1
- package/docs-site/en/profiles.html +5 -1
- package/docs-site/en/quickstart.html +5 -1
- package/docs-site/en/runtime.html +18 -1
- package/docs-site/en/scope.html +5 -1
- package/docs-site/en/security.html +5 -1
- package/docs-site/en/team.html +5 -1
- package/docs-site/en/whats-new.html +5 -1
- package/docs-site/en/workflows.html +5 -1
- package/docs-site/index.html +54 -1
- package/docs-site/mcp-approval.html +5 -1
- package/docs-site/mcp-auth.html +5 -1
- package/docs-site/mcp-servers.html +5 -1
- package/docs-site/mcp.html +5 -1
- package/docs-site/permissions.html +5 -1
- package/docs-site/profiles.html +5 -1
- package/docs-site/quickstart.html +5 -1
- package/docs-site/runtime.html +33 -1
- package/docs-site/scope.html +5 -1
- package/docs-site/security.html +5 -1
- package/docs-site/team.html +5 -1
- package/docs-site/whats-new.html +5 -1
- package/docs-site/workflows.html +5 -1
- package/evals/fixtures/mutation-provenance-record.invalid.json +17 -0
- package/evals/fixtures/mutation-provenance-record.valid.json +27 -0
- package/evals/fixtures/piagent-webui/approval-v1.invalid.json +54 -0
- package/evals/fixtures/piagent-webui/approval-v1.valid.json +58 -0
- package/evals/fixtures/piagent-webui/attachment-v1.invalid.json +25 -0
- package/evals/fixtures/piagent-webui/attachment-v1.valid.json +25 -0
- package/evals/fixtures/piagent-webui/capabilities-v1.invalid.json +51 -0
- package/evals/fixtures/piagent-webui/capabilities-v1.valid.json +122 -0
- package/evals/fixtures/piagent-webui/commit-summary-v1.invalid.json +10 -0
- package/evals/fixtures/piagent-webui/commit-summary-v1.valid.json +25 -0
- package/evals/fixtures/piagent-webui/control-command-v1.invalid.json +37 -0
- package/evals/fixtures/piagent-webui/control-command-v1.valid.json +35 -0
- package/evals/fixtures/piagent-webui/diff-v1.invalid.json +13 -0
- package/evals/fixtures/piagent-webui/diff-v1.valid.json +141 -0
- package/evals/fixtures/piagent-webui/gateway-capabilities-v1.invalid.json +35 -0
- package/evals/fixtures/piagent-webui/gateway-capabilities-v1.valid.json +35 -0
- package/evals/fixtures/piagent-webui/gateway-protocol-v1.invalid.json +14 -0
- package/evals/fixtures/piagent-webui/gateway-protocol-v1.valid.json +14 -0
- package/evals/fixtures/piagent-webui/handoff-history-v1.invalid.json +16 -0
- package/evals/fixtures/piagent-webui/handoff-history-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/model-catalog-v1.invalid.json +31 -0
- package/evals/fixtures/piagent-webui/model-catalog-v1.valid.json +43 -0
- package/evals/fixtures/piagent-webui/queue-v1.invalid.json +29 -0
- package/evals/fixtures/piagent-webui/queue-v1.valid.json +44 -0
- package/evals/fixtures/piagent-webui/recovery-history-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/recovery-history-v1.valid.json +20 -0
- package/evals/fixtures/piagent-webui/release-monitor-v1.invalid.json +12 -0
- package/evals/fixtures/piagent-webui/release-monitor-v1.valid.json +29 -0
- package/evals/fixtures/piagent-webui/review-state-v1.invalid.json +25 -0
- package/evals/fixtures/piagent-webui/review-state-v1.valid.json +38 -0
- package/evals/fixtures/piagent-webui/runtime-event-v2.invalid.json +49 -0
- package/evals/fixtures/piagent-webui/runtime-event-v2.valid.json +49 -0
- package/evals/fixtures/piagent-webui/session-catalog-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/session-catalog-v1.valid.json +58 -0
- package/evals/fixtures/piagent-webui/session-command-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +19 -0
- package/evals/fixtures/piagent-webui/snapshot-v1.invalid.json +5 -0
- package/evals/fixtures/piagent-webui/snapshot-v1.valid.json +191 -0
- package/evals/fixtures/piagent-webui/source-change-v1.invalid.json +57 -0
- package/evals/fixtures/piagent-webui/source-change-v1.valid.json +98 -0
- package/evals/fixtures/piagent-webui/source-mutation-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/source-mutation-v1.valid.json +38 -0
- package/evals/fixtures/piagent-webui/source-revert-v1.invalid.json +20 -0
- package/evals/fixtures/piagent-webui/source-revert-v1.valid.json +62 -0
- package/evals/fixtures/piagent-webui/subagent-tree-v1.invalid.json +26 -0
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +39 -0
- package/evals/fixtures/piagent-webui/task-index-v1.invalid.json +15 -0
- package/evals/fixtures/piagent-webui/task-index-v1.valid.json +15 -0
- package/evals/fixtures/piagent-webui/task-timeline-v1.invalid.json +14 -0
- package/evals/fixtures/piagent-webui/task-timeline-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/transcript-v1.invalid.json +35 -0
- package/evals/fixtures/piagent-webui/transcript-v1.valid.json +55 -0
- package/evals/fixtures/task-baseline-manifest.invalid.json +16 -0
- package/evals/fixtures/task-baseline-manifest.valid.json +41 -0
- package/evals/fixtures/verifier-file-snapshot.invalid.json +21 -0
- package/evals/fixtures/verifier-file-snapshot.valid.json +21 -0
- package/package.json +43 -4
- package/packages/piagent-core/README.md +3 -2
- package/packages/piagent-core/benchmark/benchmark-candidate.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-forensics.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-ledger.d.ts +4 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-runtime.d.ts +1 -0
- package/packages/piagent-core/capabilities/capability-core.js +2 -2
- package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
- package/packages/piagent-core/capabilities/runtime-integrity.js +190 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +80 -272
- package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
- package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
- package/packages/piagent-core/extensions/shell-reach.ts +250 -0
- package/packages/piagent-core/extensions/task-journal.js +30 -1
- package/packages/piagent-core/extensions/task-state.js +33 -36
- package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
- package/packages/piagent-core/mcp/mcp-server-catalog.js +139 -9
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +12 -2
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
- package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
- package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
- package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
- package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
- package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
- package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
- package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
- package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
- package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
- package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
- package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
- package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
- package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
- package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
- package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
- package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
- package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
- package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
- package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
- package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
- package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
- package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
- package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
- package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
- package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
- package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
- package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
- package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
- package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
- package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
- package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
- package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
- package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
- package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
- package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
- package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
- package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
- package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +93 -6
- package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
- package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
- package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
- package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
- package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
- package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
- package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
- package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
- package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +22 -27
- package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
- package/packages/piagent-core/runtime/session/token-economics.js +34 -0
- package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
- package/packages/piagent-core/security/sensitive-data.js +7 -0
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-bu7XpD9Q.js +172 -0
- package/packages/piagent-webui/dist/client/index.html +16 -0
- package/packages/piagent-webui/extension/attachment-store.ts +323 -0
- package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
- package/packages/piagent-webui/extension/lifecycle-controller.ts +423 -0
- package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
- package/packages/piagent-webui/extension/review-controller.ts +160 -0
- package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
- package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
- package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
- package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
- package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
- package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
- package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
- package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
- package/packages/piagent-webui/gateway/control-socket.ts +92 -0
- package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
- package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
- package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
- package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
- package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
- package/packages/piagent-webui/gateway/pi-host.ts +41 -0
- package/packages/piagent-webui/gateway/profile-state.ts +1 -0
- package/packages/piagent-webui/gateway/project-registry.ts +108 -0
- package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
- package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
- package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +239 -0
- package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
- package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +489 -0
- package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
- package/packages/piagent-webui/ownership/profile-state.ts +117 -0
- package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
- package/packages/piagent-webui/ownership/session-refs.ts +8 -0
- package/packages/piagent-webui/package.json +33 -0
- package/packages/piagent-webui/server/README.md +4 -0
- package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
- package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
- package/packages/piagent-webui/server/http-security.ts +27 -0
- package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
- package/packages/piagent-webui/server/loopback-server.ts +368 -0
- package/packages/piagent-webui/server/read-model-provider.ts +37 -0
- package/packages/piagent-webui/server/read-only-router.ts +105 -0
- package/packages/piagent-webui/server/session-auth.ts +88 -0
- package/packages/piagent-webui/server/sidecar-main.ts +86 -0
- package/packages/piagent-webui/server/sse-hub.ts +76 -0
- package/packages/piagent-webui/server/static-bundle.ts +40 -0
- package/packages/piagent-webui/server/transcript-projection.ts +125 -0
- package/schemas/mutation-provenance-record.schema.json +100 -0
- package/schemas/piagent-webui/approval-v1.schema.json +654 -0
- package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
- package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
- package/schemas/piagent-webui/catalog-v1.json +240 -0
- package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
- package/schemas/piagent-webui/common-v1.schema.json +479 -0
- package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
- package/schemas/piagent-webui/diff-v1.schema.json +826 -0
- package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
- package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
- package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
- package/schemas/piagent-webui/queue-v1.schema.json +99 -0
- package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
- package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
- package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
- package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
- package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
- package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
- package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
- package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
- package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
- package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
- package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
- package/schemas/task-baseline-manifest.schema.json +122 -0
- package/schemas/verifier-file-snapshot.schema.json +81 -0
- package/scripts/build-docs-site.mjs +21 -1
- package/scripts/check-architecture.mjs +1 -0
- package/scripts/check-doc-languages.mjs +19 -2
- package/scripts/check-third-party-neutrality.mjs +43 -0
- package/scripts/explain-command.mjs +240 -0
- package/scripts/install-global.sh +105 -4
- package/scripts/mcp-manage.mjs +17 -4
- package/scripts/pi-session-stats.sh +10 -2
- package/scripts/piagent-cli.mjs +30 -3
- package/scripts/piagent-dashboard.mjs +261 -0
- package/scripts/piagent-webui-launcher.mjs +121 -0
- package/scripts/setup.sh +69 -33
- package/scripts/traffic-snapshot.mjs +214 -0
- package/scripts/typescript-loader.mjs +42 -1
- package/scripts/uninstall-global.sh +53 -2
- package/scripts/update-global.mjs +33 -1
- package/scripts/verify-local.sh +257 -0
- package/scripts/verify-release-identity.mjs +4 -1
- package/scripts/verify-translation-pairing.mjs +121 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "../../extensions/local-state-path.js";
|
|
6
|
+
import { canonicalReviewValue } from "./review-state-contract.ts";
|
|
7
|
+
import { validateSourceMutationTarget, type SourceMutationAction, type SourceMutationTarget } from "./source-mutation-projection.ts";
|
|
8
|
+
import { validateSourceRevertTarget, type SourceRevertTarget } from "./source-revert-projection.ts";
|
|
9
|
+
import { readTaskBaselineManifest, taskBaselineRetentionState } from "./source-evidence-store.ts";
|
|
10
|
+
|
|
11
|
+
const MAX_RECORDS = 4_000, MAX_RECORD_BYTES = 96 * 1024;
|
|
12
|
+
const REF = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/, DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
13
|
+
const REVISION = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,159}$/, REVISION_KEYS = ["runtimeRevision", "taskRevision", "controlRevision",
|
|
14
|
+
"workspaceRevision", "indexRevision", "approvalRevision", "sessionOptionRevision", "queueRevision"];
|
|
15
|
+
export const SOURCE_MUTATION_EVIDENCE_VERSION = "piagent-webui-source-mutation-evidence-v1";
|
|
16
|
+
|
|
17
|
+
export type SourceMutationEvidenceRecord = {
|
|
18
|
+
schemaVersion: 1;
|
|
19
|
+
version: typeof SOURCE_MUTATION_EVIDENCE_VERSION;
|
|
20
|
+
recordId: string;
|
|
21
|
+
evidenceRef: string;
|
|
22
|
+
sequence: number;
|
|
23
|
+
phase: "requested" | "settled" | "rejected" | "uncertain";
|
|
24
|
+
resultCode: "mutation-requested" | "staged" | "unstaged" | "reverted" | "mutation-rejected" | "effect-unknown";
|
|
25
|
+
failureCode: string | null;
|
|
26
|
+
projectRef: string;
|
|
27
|
+
runtimeInstanceId: string;
|
|
28
|
+
sessionRef: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
taskRunId: string;
|
|
31
|
+
commandId: string;
|
|
32
|
+
idempotencyKeyDigest: string;
|
|
33
|
+
actionDigest: string;
|
|
34
|
+
action: SourceMutationAction | "source.revert";
|
|
35
|
+
target: SourceMutationTarget | SourceRevertTarget;
|
|
36
|
+
selectedHunkRefs: string[];
|
|
37
|
+
requestedAt: string;
|
|
38
|
+
recordedAt: string;
|
|
39
|
+
retentionUntil: string;
|
|
40
|
+
beforeIndexPreimage: string;
|
|
41
|
+
afterIndexPreimage: string | null;
|
|
42
|
+
beforeWorkspacePreimage: string;
|
|
43
|
+
afterWorkspacePreimage: string | null;
|
|
44
|
+
observedRevisionsBefore: Record<string, string | null>;
|
|
45
|
+
observedRevisionsAfter: Record<string, string | null> | null;
|
|
46
|
+
integrityDigest: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type AppendOptions = Omit<SourceMutationEvidenceRecord, "schemaVersion" | "version" | "recordId" | "evidenceRef" | "sequence" | "retentionUntil" | "integrityDigest"> & { projectRoot: string };
|
|
50
|
+
|
|
51
|
+
function sha(value: string): string { return createHash("sha256").update(value).digest("hex"); }
|
|
52
|
+
function evidenceDigest(value: unknown): string { return `sha256:${sha(canonicalReviewValue(value))}`; }
|
|
53
|
+
function timestamp(value: unknown): value is string {
|
|
54
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return false;
|
|
55
|
+
const parsed = Date.parse(value); return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
56
|
+
}
|
|
57
|
+
function directory(projectRoot: string, taskRunId: string): string {
|
|
58
|
+
return path.join(projectRoot, ".pi", "piagent-state", "source-evidence", `run-${sha(taskRunId)}`, "source-actions");
|
|
59
|
+
}
|
|
60
|
+
function readPrivate(projectRoot: string, file: string): Buffer {
|
|
61
|
+
const safe = resolveLocalStatePath(projectRoot, file, { label: "Source mutation evidence", kind: "file" });
|
|
62
|
+
const descriptor = fs.openSync(safe, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
63
|
+
try {
|
|
64
|
+
const stat = fs.fstatSync(descriptor);
|
|
65
|
+
if (!stat.isFile() || stat.size > MAX_RECORD_BYTES) throw new Error("mutation-record-oversized");
|
|
66
|
+
return fs.readFileSync(descriptor);
|
|
67
|
+
} finally { fs.closeSync(descriptor); }
|
|
68
|
+
}
|
|
69
|
+
function publish(projectRoot: string, target: string, bytes: Buffer): void {
|
|
70
|
+
const parent = ensurePrivateStateDirectory(projectRoot, path.dirname(target), "Source mutation evidence directory");
|
|
71
|
+
const safeTarget = resolveLocalStatePath(projectRoot, target, { label: "Source mutation evidence" });
|
|
72
|
+
const temporary = path.join(parent, `${path.basename(target)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
73
|
+
const descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
74
|
+
try { fs.writeFileSync(descriptor, bytes); fs.fsyncSync(descriptor); fs.fchmodSync(descriptor, 0o600); }
|
|
75
|
+
finally { fs.closeSync(descriptor); }
|
|
76
|
+
try { fs.linkSync(temporary, safeTarget); }
|
|
77
|
+
catch (error) {
|
|
78
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST" || !readPrivate(projectRoot, safeTarget).equals(bytes)) throw error;
|
|
79
|
+
} finally { fs.rmSync(temporary, { force: true }); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function validateSourceMutationEvidence(value: unknown): SourceMutationEvidenceRecord {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("mutation-record-invalid");
|
|
84
|
+
const record = value as SourceMutationEvidenceRecord;
|
|
85
|
+
const keys = ["schemaVersion", "version", "recordId", "evidenceRef", "sequence", "phase", "resultCode", "projectRef", "runtimeInstanceId", "sessionRef",
|
|
86
|
+
"taskId", "taskRunId", "commandId", "idempotencyKeyDigest", "actionDigest", "action", "target", "selectedHunkRefs", "failureCode", "requestedAt", "recordedAt", "retentionUntil",
|
|
87
|
+
"beforeIndexPreimage", "afterIndexPreimage", "beforeWorkspacePreimage", "afterWorkspacePreimage", "observedRevisionsBefore", "observedRevisionsAfter", "integrityDigest"];
|
|
88
|
+
if (canonicalReviewValue(Object.keys(record).sort()) !== canonicalReviewValue(keys.sort()) || record.schemaVersion !== 1
|
|
89
|
+
|| record.version !== SOURCE_MUTATION_EVIDENCE_VERSION || ![record.recordId, record.evidenceRef, record.projectRef, record.runtimeInstanceId,
|
|
90
|
+
record.sessionRef, record.taskId, record.taskRunId, record.commandId].every((item) => REF.test(item))
|
|
91
|
+
|| !Number.isInteger(record.sequence) || record.sequence < 1 || record.sequence > MAX_RECORDS
|
|
92
|
+
|| ![record.idempotencyKeyDigest, record.actionDigest, record.beforeIndexPreimage, record.integrityDigest].every((item) => DIGEST.test(item))
|
|
93
|
+
|| !/^wt-content-v2:[a-f0-9]{64}$/.test(record.beforeWorkspacePreimage)
|
|
94
|
+
|| record.afterIndexPreimage !== null && !DIGEST.test(record.afterIndexPreimage)
|
|
95
|
+
|| record.afterWorkspacePreimage !== null && !/^wt-content-v2:[a-f0-9]{64}$/.test(record.afterWorkspacePreimage)
|
|
96
|
+
|| !Array.isArray(record.selectedHunkRefs) || record.selectedHunkRefs.length > 128
|
|
97
|
+
|| new Set(record.selectedHunkRefs).size !== record.selectedHunkRefs.length
|
|
98
|
+
|| record.selectedHunkRefs.some((item) => !REF.test(item) || !record.target.hunkRefs.includes(item))
|
|
99
|
+
|| ![record.requestedAt, record.recordedAt, record.retentionUntil].every(timestamp)
|
|
100
|
+
|| !(record.failureCode === null || /^[a-z][a-z0-9.-]{0,95}$/.test(record.failureCode))
|
|
101
|
+
|| !["source.stage", "source.unstage", "source.revert"].includes(record.action)) throw new Error("mutation-record-invalid");
|
|
102
|
+
const settledResult = record.action === "source.stage" ? "staged" : record.action === "source.unstage" ? "unstaged" : "reverted";
|
|
103
|
+
const phaseResult = { requested: "mutation-requested", settled: settledResult,
|
|
104
|
+
rejected: "mutation-rejected", uncertain: "effect-unknown" }[record.phase];
|
|
105
|
+
if (record.resultCode !== phaseResult || record.phase === "requested" && (record.afterIndexPreimage !== null || record.afterWorkspacePreimage !== null)
|
|
106
|
+
|| record.phase === "settled" && (!record.afterIndexPreimage || !record.afterWorkspacePreimage)
|
|
107
|
+
|| ["requested", "settled"].includes(record.phase) !== (record.failureCode === null)) throw new Error("mutation-record-phase-invalid");
|
|
108
|
+
if (record.action === "source.revert") validateSourceRevertTarget(record.target); else validateSourceMutationTarget(record.target);
|
|
109
|
+
for (const revisions of [record.observedRevisionsBefore, record.observedRevisionsAfter].filter(Boolean) as Array<Record<string, string | null>>) {
|
|
110
|
+
if (canonicalReviewValue(Object.keys(revisions).sort()) !== canonicalReviewValue([...REVISION_KEYS].sort())
|
|
111
|
+
|| !REVISION.test(String(revisions.runtimeRevision ?? "")) || REVISION_KEYS.slice(1).some((key) => revisions[key] !== null && !REVISION.test(String(revisions[key]))))
|
|
112
|
+
throw new Error("mutation-record-revisions-invalid");
|
|
113
|
+
}
|
|
114
|
+
if (record.phase === "requested" ? record.observedRevisionsAfter !== null : record.observedRevisionsAfter === null) throw new Error("mutation-record-revisions-invalid");
|
|
115
|
+
const { integrityDigest, ...payload } = record;
|
|
116
|
+
if (evidenceDigest(payload) !== integrityDigest) throw new Error("mutation-record-integrity-invalid");
|
|
117
|
+
return structuredClone(record);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function readSourceMutationEvidence(projectRoot: string, taskRunId: string): { records: SourceMutationEvidenceRecord[]; corruptions: string[] } {
|
|
121
|
+
const root = directory(projectRoot, taskRunId);
|
|
122
|
+
let files: string[];
|
|
123
|
+
try { files = fs.readdirSync(resolveLocalStatePath(projectRoot, root, { label: "Source mutation evidence", kind: "directory" })).filter((file) => file.endsWith(".json")).sort(); }
|
|
124
|
+
catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? { records: [], corruptions: [] } : { records: [], corruptions: ["mutation-evidence-directory-unavailable"] }; }
|
|
125
|
+
if (files.length > MAX_RECORDS) return { records: [], corruptions: ["mutation-evidence-record-limit"] };
|
|
126
|
+
const records: SourceMutationEvidenceRecord[] = [], corruptions: string[] = [];
|
|
127
|
+
for (const file of files) try {
|
|
128
|
+
const record = validateSourceMutationEvidence(JSON.parse(readPrivate(projectRoot, path.join(root, file)).toString("utf8")));
|
|
129
|
+
if (file !== `${record.recordId}.json` || record.taskRunId !== taskRunId) throw new Error("mutation-record-identity-invalid");
|
|
130
|
+
records.push(record);
|
|
131
|
+
} catch { corruptions.push(`mutation.${sha(file)}`); }
|
|
132
|
+
return { records: corruptions.length ? [] : records.sort((left, right) => left.sequence - right.sequence), corruptions };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function appendSourceMutationEvidence(options: AppendOptions): SourceMutationEvidenceRecord {
|
|
136
|
+
const manifest = readTaskBaselineManifest(options.projectRoot, options.taskRunId);
|
|
137
|
+
if (!manifest || manifest.taskId !== options.taskId || manifest.captureState !== "current"
|
|
138
|
+
|| taskBaselineRetentionState(manifest, new Date(options.recordedAt)) !== "active") throw new Error("mutation-baseline-unavailable");
|
|
139
|
+
const existing = readSourceMutationEvidence(options.projectRoot, options.taskRunId);
|
|
140
|
+
if (existing.corruptions.length || existing.records.length >= MAX_RECORDS) throw new Error("mutation-evidence-unavailable");
|
|
141
|
+
const identity = evidenceDigest({ commandId: options.commandId, actionDigest: options.actionDigest, phase: options.phase });
|
|
142
|
+
const recordId = `mutation.${identity.slice(7)}`;
|
|
143
|
+
const duplicate = existing.records.find((record) => record.recordId === recordId);
|
|
144
|
+
if (duplicate) {
|
|
145
|
+
const expected = { phase: options.phase, resultCode: options.resultCode, failureCode: options.failureCode, selectedHunkRefs: options.selectedHunkRefs,
|
|
146
|
+
commandId: options.commandId, idempotencyKeyDigest: options.idempotencyKeyDigest,
|
|
147
|
+
actionDigest: options.actionDigest, action: options.action, target: options.target, beforeIndexPreimage: options.beforeIndexPreimage,
|
|
148
|
+
afterIndexPreimage: options.afterIndexPreimage, beforeWorkspacePreimage: options.beforeWorkspacePreimage,
|
|
149
|
+
afterWorkspacePreimage: options.afterWorkspacePreimage, observedRevisionsBefore: options.observedRevisionsBefore,
|
|
150
|
+
observedRevisionsAfter: options.observedRevisionsAfter };
|
|
151
|
+
const actual = Object.fromEntries(Object.keys(expected).map((key) => [key, (duplicate as any)[key]]));
|
|
152
|
+
if (canonicalReviewValue(actual) !== canonicalReviewValue(expected)) throw new Error("mutation-evidence-identity-collision");
|
|
153
|
+
return validateSourceMutationEvidence(duplicate);
|
|
154
|
+
}
|
|
155
|
+
const payload = { schemaVersion: 1 as const, version: SOURCE_MUTATION_EVIDENCE_VERSION, recordId, evidenceRef: `mutation-evidence.${identity.slice(7)}`,
|
|
156
|
+
sequence: (existing.records.at(-1)?.sequence ?? 0) + 1, phase: options.phase, resultCode: options.resultCode, failureCode: options.failureCode,
|
|
157
|
+
projectRef: options.projectRef, runtimeInstanceId: options.runtimeInstanceId, sessionRef: options.sessionRef, taskId: options.taskId,
|
|
158
|
+
taskRunId: options.taskRunId, commandId: options.commandId, idempotencyKeyDigest: options.idempotencyKeyDigest, actionDigest: options.actionDigest,
|
|
159
|
+
action: options.action, target: structuredClone(options.target), selectedHunkRefs: [...options.selectedHunkRefs], requestedAt: options.requestedAt, recordedAt: options.recordedAt,
|
|
160
|
+
retentionUntil: manifest.retentionUntil, beforeIndexPreimage: options.beforeIndexPreimage, afterIndexPreimage: options.afterIndexPreimage,
|
|
161
|
+
beforeWorkspacePreimage: options.beforeWorkspacePreimage, afterWorkspacePreimage: options.afterWorkspacePreimage,
|
|
162
|
+
observedRevisionsBefore: structuredClone(options.observedRevisionsBefore), observedRevisionsAfter: structuredClone(options.observedRevisionsAfter) };
|
|
163
|
+
const record = validateSourceMutationEvidence({ ...payload, integrityDigest: evidenceDigest(payload) });
|
|
164
|
+
const bytes = Buffer.from(`${JSON.stringify(record, null, 2)}\n`);
|
|
165
|
+
if (bytes.length > MAX_RECORD_BYTES) throw new Error("mutation-record-oversized");
|
|
166
|
+
publish(options.projectRoot, path.join(directory(options.projectRoot, options.taskRunId), `${record.recordId}.json`), bytes);
|
|
167
|
+
return record;
|
|
168
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { collectSelectedSourceFileState, type SourceChangeDocument, type SourceProjectionOptions, type WebUiIdentity } from "./source-change-projection.ts";
|
|
5
|
+
|
|
6
|
+
const REF = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/, REVISION = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,159}$/,
|
|
7
|
+
DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
8
|
+
|
|
9
|
+
export type SourceOpenTarget = { fileRef: string; taskRevision: string; workspaceRevision: string; fileRevision: string; contentDigest: string };
|
|
10
|
+
export type SourceOpenAuthority = { projectRoot: string; repoRoot: string; repoPath: string; absolutePath: string; target: SourceOpenTarget };
|
|
11
|
+
|
|
12
|
+
function inside(root: string, candidate: string): boolean {
|
|
13
|
+
const relative = path.relative(root, candidate);
|
|
14
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function resolveSourceOpenTarget(options: { cwd: string; identity: WebUiIdentity; sourceView: SourceChangeDocument;
|
|
18
|
+
fileRef: string; taskRevision: string; workspaceRevision: string; generatedAt?: string;
|
|
19
|
+
isProtectedPath?: SourceProjectionOptions["isProtectedPath"]; timeoutMs?: number; maxGitBytes?: number }): Promise<SourceOpenAuthority | null> {
|
|
20
|
+
if (options.sourceView.view !== "working-tree" || options.sourceView.availability.state !== "current" || !REF.test(options.fileRef)
|
|
21
|
+
|| !REVISION.test(options.taskRevision) || !REVISION.test(options.workspaceRevision)) return null;
|
|
22
|
+
const file = options.sourceView.files.find((candidate: any) => candidate.fileRef === options.fileRef) as Record<string, any> | undefined;
|
|
23
|
+
if (!file || file.pathDisplay !== "exact-safe" || file.oldPath !== null || ["D", "R", "C"].includes(String(file.status))
|
|
24
|
+
|| file.content?.access !== "available" || file.content?.kind !== "text" || !DIGEST.test(String(file.currentDigest ?? ""))) return null;
|
|
25
|
+
const selected = await collectSelectedSourceFileState({ cwd: options.cwd, identity: options.identity, taskRevision: options.taskRevision,
|
|
26
|
+
generatedAt: options.generatedAt, isProtectedPath: options.isProtectedPath, timeoutMs: options.timeoutMs, maxGitBytes: options.maxGitBytes },
|
|
27
|
+
"working-tree", options.fileRef, String(file.basisRef), [file.path]);
|
|
28
|
+
const selectedContent = selected?.file.content as { access?: unknown; kind?: unknown } | undefined;
|
|
29
|
+
if (!selected || selected.file.fileRevision !== file.fileRevision || selected.record.kind !== "ordinary" || selected.record.oldPath
|
|
30
|
+
|| selectedContent?.access !== "available" || selectedContent.kind !== "text") return null;
|
|
31
|
+
const repoPath = selected.record.path.value; if (!repoPath || options.isProtectedPath?.(selected.snapshot.repoRoot, repoPath) === true) return null;
|
|
32
|
+
const projectRoot = fs.realpathSync.native(options.cwd), repoRoot = fs.realpathSync.native(selected.snapshot.repoRoot);
|
|
33
|
+
const unresolved = path.resolve(repoRoot, repoPath); if (!inside(projectRoot, unresolved) || !inside(repoRoot, unresolved)) return null;
|
|
34
|
+
const stat = fs.lstatSync(unresolved); if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
35
|
+
const absolutePath = fs.realpathSync.native(unresolved); if (absolutePath !== unresolved || !inside(projectRoot, absolutePath)) return null;
|
|
36
|
+
return { projectRoot, repoRoot, repoPath, absolutePath, target: { fileRef: options.fileRef, taskRevision: options.taskRevision,
|
|
37
|
+
workspaceRevision: options.workspaceRevision, fileRevision: String(file.fileRevision), contentDigest: String(file.currentDigest) } };
|
|
38
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { canonicalReviewValue } from "./review-state-contract.ts";
|
|
4
|
+
import { collectExactGitPatchAuthority, type DiffDocument, type ExactGitPatchAuthority } from "./diff-projection.ts";
|
|
5
|
+
import { deriveReviewTarget } from "./review-state-projection.ts";
|
|
6
|
+
import { collectSelectedSourceFileState, type SourceChangeDocument, type SourceProjectionOptions, type WebUiIdentity } from "./source-change-projection.ts";
|
|
7
|
+
import { collectIndexPreimage, collectSelectedWorkspacePreimage } from "./source-mutation-projection.ts";
|
|
8
|
+
|
|
9
|
+
const REF = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/, REVISION = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,159}$/,
|
|
10
|
+
DIGEST = /^sha256:[a-f0-9]{64}$/, WORKSPACE = /^wt-content-v2:[a-f0-9]{64}$/,
|
|
11
|
+
SAFE_PATH = /^(?!\/)(?![A-Za-z]:)(?!.*(?:^|\/)\.\.(?:\/|$))[^\u0000-\u001F\u007F\\]{1,1024}$/;
|
|
12
|
+
|
|
13
|
+
export type SourceRevertTarget = {
|
|
14
|
+
view: "working-tree"; repoRef: string; fileRef: string; diffRef: string; status: "M"; path: string; oldPath: null;
|
|
15
|
+
effect: "restore-worktree-from-index"; taskRevision: string; workspaceRevision: string; indexRevision: string;
|
|
16
|
+
viewRevision: string; fileRevision: string; workspacePreimage: string; indexPreimage: string; patchPreimage: string;
|
|
17
|
+
contentDigest: string; hunkRefs: string[]; previewRef: string; confirmedPreviewDigest: string; expiresAt: string;
|
|
18
|
+
summary: { files: 1; hunks: number; additionsDiscarded: number; deletionsRestored: number;
|
|
19
|
+
effect: "discard-unstaged-keep-index"; recovery: "not-guaranteed" };
|
|
20
|
+
};
|
|
21
|
+
export type SourceRevertProjection = {
|
|
22
|
+
schemaVersion: 1; version: "piagent-webui-source-revert-v1"; generatedAt: string; identity: WebUiIdentity;
|
|
23
|
+
action: "source.revert"; state: "ready" | "unavailable"; target: SourceRevertTarget | null;
|
|
24
|
+
preview: { basis: "index-to-working-tree"; hunks: RevertPreviewHunk[]; truncated: false } | null; reasonCode: string | null;
|
|
25
|
+
health: { state: "ok" | "unavailable" | "error"; reasonCode: string | null; message: string | null };
|
|
26
|
+
};
|
|
27
|
+
type RevertPreviewLine = { kind: "added" | "deleted" | "context" | "meta"; marker: "+" | "-" | " " | "\\"; text: string };
|
|
28
|
+
type RevertPreviewHunk = { hunkRef: string; header: string; lines: RevertPreviewLine[] };
|
|
29
|
+
export type SourceRevertAuthority = {
|
|
30
|
+
repoRoot: string; repoPath: string; indexObject: string; indexMode: "100644" | "100755";
|
|
31
|
+
target: SourceRevertTarget; patchAuthority: ExactGitPatchAuthority;
|
|
32
|
+
};
|
|
33
|
+
type CollectOptions = {
|
|
34
|
+
cwd: string; identity: WebUiIdentity; sourceView: SourceChangeDocument; taskView: SourceChangeDocument | null; diff: DiffDocument;
|
|
35
|
+
taskRevision: string; workspaceRevision: string; indexRevision: string; selectedHunkRefs?: string[]; generatedAt?: string;
|
|
36
|
+
confirmationKey: Buffer;
|
|
37
|
+
isProtectedPath?: SourceProjectionOptions["isProtectedPath"]; guardAvailable?: boolean; timeoutMs?: number; maxGitBytes?: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function confirmationDigest(key: Buffer, value: unknown): string {
|
|
41
|
+
return `sha256:${createHmac("sha256", key).update(canonicalReviewValue(value)).digest("hex")}`;
|
|
42
|
+
}
|
|
43
|
+
function previewHunk(hunk: ExactGitPatchAuthority["hunks"][number]): RevertPreviewHunk | null {
|
|
44
|
+
const text = hunk.bytes.toString("utf8");
|
|
45
|
+
if (!Buffer.from(text, "utf8").equals(hunk.bytes)) return null;
|
|
46
|
+
const raw = text.split("\n"); if (raw.at(-1) === "") raw.pop();
|
|
47
|
+
const header = raw.shift() ?? ""; if (!/^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(header) || header.length > 1024) return null;
|
|
48
|
+
const lines: RevertPreviewLine[] = [];
|
|
49
|
+
for (const line of raw) {
|
|
50
|
+
const marker = line.startsWith("+") ? "+" : line.startsWith("-") ? "-" : line.startsWith("\\") ? "\\" : " ";
|
|
51
|
+
const value = line.slice(1); if (value.length > 16_384 || /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(value)) return null;
|
|
52
|
+
lines.push({ kind: marker === "+" ? "added" : marker === "-" ? "deleted" : marker === "\\" ? "meta" : "context", marker, text: value });
|
|
53
|
+
}
|
|
54
|
+
return { hunkRef: hunk.hunkRef, header, lines };
|
|
55
|
+
}
|
|
56
|
+
function unavailable(options: CollectOptions, reasonCode: string): { projection: SourceRevertProjection; authority: null } {
|
|
57
|
+
const message = "A confirmed exact Working Tree revert is unavailable";
|
|
58
|
+
return { projection: { schemaVersion: 1, version: "piagent-webui-source-revert-v1", generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
59
|
+
identity: structuredClone(options.identity), action: "source.revert", state: "unavailable", target: null, preview: null, reasonCode,
|
|
60
|
+
health: { state: "unavailable", reasonCode, message } }, authority: null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validateSourceRevertTarget(value: unknown): SourceRevertTarget {
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("source-revert-target-invalid");
|
|
65
|
+
const target = value as SourceRevertTarget;
|
|
66
|
+
const keys = ["view", "repoRef", "fileRef", "diffRef", "status", "path", "oldPath", "effect", "taskRevision", "workspaceRevision",
|
|
67
|
+
"indexRevision", "viewRevision", "fileRevision", "workspacePreimage", "indexPreimage", "patchPreimage", "contentDigest", "hunkRefs",
|
|
68
|
+
"previewRef", "confirmedPreviewDigest", "expiresAt", "summary"];
|
|
69
|
+
if (canonicalReviewValue(Object.keys(target).sort()) !== canonicalReviewValue(keys.sort()) || target.view !== "working-tree" || target.status !== "M"
|
|
70
|
+
|| target.oldPath !== null || target.effect !== "restore-worktree-from-index" || !SAFE_PATH.test(target.path)
|
|
71
|
+
|| ![target.repoRef, target.fileRef, target.diffRef, target.previewRef].every((item) => REF.test(item))
|
|
72
|
+
|| ![target.taskRevision, target.workspaceRevision, target.indexRevision, target.viewRevision, target.fileRevision].every((item) => REVISION.test(item))
|
|
73
|
+
|| !WORKSPACE.test(target.workspacePreimage) || ![target.indexPreimage, target.patchPreimage, target.contentDigest, target.confirmedPreviewDigest].every((item) => DIGEST.test(item))
|
|
74
|
+
|| !Array.isArray(target.hunkRefs) || target.hunkRefs.length > 1 || new Set(target.hunkRefs).size !== target.hunkRefs.length
|
|
75
|
+
|| target.hunkRefs.some((item) => !REF.test(item)) || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(target.expiresAt)
|
|
76
|
+
|| !target.summary || target.summary.files !== 1 || !Number.isInteger(target.summary.hunks) || target.summary.hunks < 1 || target.summary.hunks > 128
|
|
77
|
+
|| ![target.summary.additionsDiscarded, target.summary.deletionsRestored].every((item) => Number.isInteger(item) && item >= 0 && item <= 65_536)
|
|
78
|
+
|| target.summary.effect !== "discard-unstaged-keep-index" || target.summary.recovery !== "not-guaranteed") throw new Error("source-revert-target-invalid");
|
|
79
|
+
const parsed = Date.parse(target.expiresAt); if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== target.expiresAt) throw new Error("source-revert-target-invalid");
|
|
80
|
+
return structuredClone(target);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function collectSourceRevertPreview(options: CollectOptions): Promise<{ projection: SourceRevertProjection; authority: SourceRevertAuthority | null }> {
|
|
84
|
+
const selectedHunkRefs = options.selectedHunkRefs ?? [];
|
|
85
|
+
if (!Buffer.isBuffer(options.confirmationKey) || options.confirmationKey.length < 32) return unavailable(options, "revert-confirmation-authority-unavailable");
|
|
86
|
+
if (options.guardAvailable !== true) return unavailable(options, "mutation-guard-unavailable");
|
|
87
|
+
if (options.sourceView.view !== "working-tree" || options.sourceView.availability.state !== "current" || !options.identity.taskId || !options.identity.taskRunId
|
|
88
|
+
|| !options.taskRevision || !options.workspaceRevision || !options.indexRevision || selectedHunkRefs.length > 1 || new Set(selectedHunkRefs).size !== selectedHunkRefs.length)
|
|
89
|
+
return unavailable(options, "revert-authority-unavailable");
|
|
90
|
+
const file = options.sourceView.files.find((candidate: any) => candidate.fileRef === (options.diff as any).file?.fileRef) as Record<string, any> | undefined;
|
|
91
|
+
if (!file || file.status !== "M" || file.git?.worktreeStatus !== "M" || file.oldPath !== null || file.pathDisplay !== "exact-safe"
|
|
92
|
+
|| file.content?.kind !== "text" || file.content?.access !== "available") return unavailable(options, "revert-target-unavailable");
|
|
93
|
+
const taskFile = options.taskView?.files.find((candidate: any) => candidate.path === file.path && candidate.currentDigest === file.currentDigest) as Record<string, any> | undefined;
|
|
94
|
+
if (!taskFile || taskFile.provenance?.classification !== "runtime-observed-agent" || taskFile.provenance?.evidence !== "exact"
|
|
95
|
+
|| !Array.isArray(taskFile.provenance?.mutationEvidenceRefs) || taskFile.provenance.mutationEvidenceRefs.length === 0)
|
|
96
|
+
return unavailable(options, "revert-provenance-unavailable");
|
|
97
|
+
const review = deriveReviewTarget({ diff: options.diff, taskId: options.identity.taskId, taskRunId: options.identity.taskRunId,
|
|
98
|
+
taskRevision: options.taskRevision, workspaceRevision: options.workspaceRevision, indexRevision: options.indexRevision });
|
|
99
|
+
if (!review || review.view !== "working-tree" || review.fileRef !== file.fileRef || (options.diff as any).truncation?.truncated !== false
|
|
100
|
+
|| (options.diff as any).redaction?.applied !== false) return unavailable(options, "revert-preview-incomplete");
|
|
101
|
+
const selected = await collectSelectedSourceFileState({ cwd: options.cwd, identity: options.identity, taskRevision: options.taskRevision,
|
|
102
|
+
generatedAt: options.generatedAt, isProtectedPath: options.isProtectedPath, timeoutMs: options.timeoutMs, maxGitBytes: options.maxGitBytes },
|
|
103
|
+
"working-tree", file.fileRef, String(file.basisRef), [file.path]);
|
|
104
|
+
if ((selected?.file as Record<string, any> | undefined)?.content?.access === "protected") return unavailable(options, "protected-path");
|
|
105
|
+
if (!selected || selected.file.fileRevision !== file.fileRevision || selected.record.kind !== "ordinary" || selected.record.oldPath
|
|
106
|
+
|| selected.record.worktreeStatus !== "M" || !selected.record.indexObject || !["100644", "100755"].includes(String(selected.record.indexMode)))
|
|
107
|
+
return unavailable(options, "revert-target-stale");
|
|
108
|
+
const repoPath = selected.record.path.value;
|
|
109
|
+
if (!repoPath || options.isProtectedPath?.(selected.snapshot.repoRoot, repoPath) === true) return unavailable(options, "protected-path");
|
|
110
|
+
try {
|
|
111
|
+
const patchAuthority = await collectExactGitPatchAuthority({ snapshot: selected.snapshot, record: selected.record, view: "unstaged",
|
|
112
|
+
fileRef: String(file.fileRef), timeoutMs: options.timeoutMs, maxBytes: options.maxGitBytes });
|
|
113
|
+
if (!patchAuthority || patchAuthority.hunks.length === 0 || patchAuthority.hunks.length > 128
|
|
114
|
+
|| selectedHunkRefs.some((item) => !patchAuthority.hunks.some((hunk) => hunk.hunkRef === item))) return unavailable(options, "revert-hunk-unavailable");
|
|
115
|
+
const selectedAuthorityHunks = patchAuthority.hunks.filter((hunk) => selectedHunkRefs.length === 0 || selectedHunkRefs.includes(hunk.hunkRef));
|
|
116
|
+
const selectedRefs = selectedAuthorityHunks.map((hunk) => hunk.hunkRef);
|
|
117
|
+
if (selectedHunkRefs.length && canonicalReviewValue(selectedRefs) !== canonicalReviewValue(selectedHunkRefs)) return unavailable(options, "revert-hunk-unavailable");
|
|
118
|
+
const previewHunks = selectedAuthorityHunks.map(previewHunk);
|
|
119
|
+
if (previewHunks.some((hunk) => !hunk) || previewHunks.reduce((count, hunk) => count + (hunk?.lines.length ?? 0), 0) > 5_000)
|
|
120
|
+
return unavailable(options, "revert-preview-incomplete");
|
|
121
|
+
const exactPreviewHunks = previewHunks as RevertPreviewHunk[];
|
|
122
|
+
const workspacePreimage = collectSelectedWorkspacePreimage(selected.snapshot.repoRoot, [repoPath]);
|
|
123
|
+
const indexPreimage = await collectIndexPreimage(selected.snapshot.repoRoot, options.timeoutMs, options.maxGitBytes);
|
|
124
|
+
const generatedAt = options.generatedAt ?? new Date().toISOString(), expiresAt = new Date(Date.parse(generatedAt) + 5 * 60_000).toISOString();
|
|
125
|
+
const confirmation = { projectRef: options.identity.projectRef, sessionRef: options.identity.sessionRef, taskRunId: options.identity.taskRunId,
|
|
126
|
+
fileRef: file.fileRef, hunkRefs: selectedHunkRefs, taskRevision: options.taskRevision, workspaceRevision: options.workspaceRevision,
|
|
127
|
+
indexRevision: options.indexRevision, viewRevision: review.viewRevision, fileRevision: review.fileRevision,
|
|
128
|
+
workspacePreimage, indexPreimage, patchPreimage: review.patchPreimage, contentDigest: review.contentDigest };
|
|
129
|
+
const confirmedPreviewDigest = confirmationDigest(options.confirmationKey,
|
|
130
|
+
{ domain: "piagent-webui-source-revert-preview-v1", confirmation });
|
|
131
|
+
const target: SourceRevertTarget = { view: "working-tree", repoRef: String(file.repoRef), fileRef: String(file.fileRef), diffRef: review.diffRef,
|
|
132
|
+
status: "M", path: String(file.path), oldPath: null, effect: "restore-worktree-from-index", taskRevision: options.taskRevision,
|
|
133
|
+
workspaceRevision: options.workspaceRevision, indexRevision: options.indexRevision, viewRevision: review.viewRevision,
|
|
134
|
+
fileRevision: review.fileRevision, workspacePreimage, indexPreimage, patchPreimage: review.patchPreimage, contentDigest: review.contentDigest,
|
|
135
|
+
hunkRefs: [...selectedHunkRefs], previewRef: `revert-preview.${confirmedPreviewDigest.slice(7)}`, confirmedPreviewDigest, expiresAt,
|
|
136
|
+
summary: { files: 1, hunks: exactPreviewHunks.length,
|
|
137
|
+
additionsDiscarded: exactPreviewHunks.reduce((count, hunk) => count + hunk.lines.filter((line) => line.kind === "added").length, 0),
|
|
138
|
+
deletionsRestored: exactPreviewHunks.reduce((count, hunk) => count + hunk.lines.filter((line) => line.kind === "deleted").length, 0),
|
|
139
|
+
effect: "discard-unstaged-keep-index", recovery: "not-guaranteed" } };
|
|
140
|
+
validateSourceRevertTarget(target);
|
|
141
|
+
return { projection: { schemaVersion: 1, version: "piagent-webui-source-revert-v1", generatedAt, identity: structuredClone(options.identity),
|
|
142
|
+
action: "source.revert", state: "ready", target, preview: { basis: "index-to-working-tree", hunks: exactPreviewHunks, truncated: false },
|
|
143
|
+
reasonCode: null, health: { state: "ok", reasonCode: null, message: null } },
|
|
144
|
+
authority: { repoRoot: selected.snapshot.repoRoot, repoPath, indexObject: selected.record.indexObject,
|
|
145
|
+
indexMode: selected.record.indexMode as "100644" | "100755", target, patchAuthority } };
|
|
146
|
+
} catch { return unavailable(options, "revert-preimage-unavailable"); }
|
|
147
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { captureTaskBaselineManifest } from "./source-evidence-store.ts";
|
|
2
|
+
|
|
3
|
+
export async function captureTaskStartBaseline(input: {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
taskId: string;
|
|
6
|
+
taskRunId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
capturedAt: string;
|
|
9
|
+
baselineTreeDigest: string;
|
|
10
|
+
protectedPaths: string[];
|
|
11
|
+
matchesProtectedPath: (path: string, patterns: string[]) => unknown;
|
|
12
|
+
}): Promise<void> {
|
|
13
|
+
await captureTaskBaselineManifest({
|
|
14
|
+
projectRoot: input.projectRoot,
|
|
15
|
+
taskId: input.taskId,
|
|
16
|
+
taskRunId: input.taskRunId,
|
|
17
|
+
sessionId: input.sessionId,
|
|
18
|
+
capturedAt: input.capturedAt,
|
|
19
|
+
baselineTreeDigest: input.baselineTreeDigest,
|
|
20
|
+
isProtectedProjectPath: (projectPath) => Boolean(input.matchesProtectedPath(projectPath, input.protectedPaths))
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { redactSensitiveText } from "../../extensions/redaction-core.js";
|
|
4
|
+
import { inspectTaskResumeState } from "../recovery/resume-state.ts";
|
|
5
|
+
import { inspectBoundedContextTelemetry } from "./context-telemetry-inspection.ts";
|
|
6
|
+
import { taskRunOpaqueRef } from "./task-run-index.ts";
|
|
7
|
+
|
|
8
|
+
const MAX_EVENTS = 300;
|
|
9
|
+
const MAX_TELEMETRY = 5_000;
|
|
10
|
+
|
|
11
|
+
type Task = {
|
|
12
|
+
taskId: string;
|
|
13
|
+
taskRunId: string;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
};
|
|
17
|
+
type Identity = {
|
|
18
|
+
projectRef: string;
|
|
19
|
+
runtimeInstanceId: string;
|
|
20
|
+
sessionRef: string;
|
|
21
|
+
taskId: string;
|
|
22
|
+
taskRunId: string;
|
|
23
|
+
agentOperationId: null;
|
|
24
|
+
toolCallId: null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function opaque(prefix: string, value: string): string {
|
|
28
|
+
return `${prefix}.${createHash("sha256").update(`piagent-webui-recovery-history-v1\0${value}`).digest("hex").slice(0, 48)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function display(value: unknown, maximum = 160): string {
|
|
32
|
+
return redactSensitiveText(String(value ?? "")).text
|
|
33
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
34
|
+
.replace(/\s+/g, " ")
|
|
35
|
+
.trim()
|
|
36
|
+
.slice(0, maximum);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function timestamp(value: unknown): value is string {
|
|
40
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
|
|
41
|
+
&& Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function boundedInteger(value: unknown, maximum = 1_000_000_000): number | null {
|
|
45
|
+
return Number.isInteger(value) && Number(value) >= 0 ? Math.min(Number(value), maximum) : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function recoverySummary(cwd: string, task: Task) {
|
|
49
|
+
const resume = inspectTaskResumeState(cwd, task as any, task.sessionId);
|
|
50
|
+
const latestCheckpointRef = resume.latestCheckpoint
|
|
51
|
+
? opaque("checkpoint", `${task.taskRunId}\0${resume.latestCheckpoint.checkpointId}`)
|
|
52
|
+
: null;
|
|
53
|
+
return {
|
|
54
|
+
decision: resume.decision,
|
|
55
|
+
enforcementSafe: resume.enforcementSafe,
|
|
56
|
+
latestCheckpointRef,
|
|
57
|
+
verifierState: resume.verifierEvidenceCurrent ? "current" : resume.staleVerifierEvidence ? "stale" : "not-current",
|
|
58
|
+
invalidatedFileCount: Math.min(100_000, resume.invalidatedVerifierFiles.length),
|
|
59
|
+
invalidatedFilesKnown: resume.invalidatedVerifierFilesKnown,
|
|
60
|
+
handoffState: !resume.handoff.exists ? "none" : resume.handoff.valid ? "available" : "invalid",
|
|
61
|
+
reasonCode: `recovery-${resume.decision}`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function unavailable(identity: Identity, runRef: string, generatedAt: string, reasonCode = "recovery-history-unavailable") {
|
|
66
|
+
return {
|
|
67
|
+
schemaVersion: 1,
|
|
68
|
+
version: "piagent-webui-recovery-history-v1",
|
|
69
|
+
generatedAt,
|
|
70
|
+
identity: structuredClone(identity),
|
|
71
|
+
runRef,
|
|
72
|
+
state: "unavailable",
|
|
73
|
+
historyRevision: null,
|
|
74
|
+
completeness: "unknown",
|
|
75
|
+
summary: { contextCompactions: 0, toolResultCompactions: 0, compactedToolResults: 0 },
|
|
76
|
+
recovery: { decision: "unknown", enforcementSafe: null, latestCheckpointRef: null, verifierState: "unknown",
|
|
77
|
+
invalidatedFileCount: null, invalidatedFilesKnown: false, handoffState: "unknown", reasonCode },
|
|
78
|
+
retainedContent: { access: "omitted", exposed: false, reasonCode: "protected-runtime-evidence" },
|
|
79
|
+
events: [],
|
|
80
|
+
page: { total: 0, returned: 0, truncated: false },
|
|
81
|
+
warnings: [],
|
|
82
|
+
health: { state: "unavailable", reasonCode, message: "Compaction and recovery history is unavailable." }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function projectTaskCompactionHistory(input: {
|
|
87
|
+
cwd: string;
|
|
88
|
+
task: Task;
|
|
89
|
+
identity: Identity;
|
|
90
|
+
generatedAt?: string;
|
|
91
|
+
}): Record<string, any> {
|
|
92
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
93
|
+
const runRef = taskRunOpaqueRef(input.task.taskRunId);
|
|
94
|
+
try {
|
|
95
|
+
const inspected = inspectBoundedContextTelemetry(input.cwd, { limit: MAX_TELEMETRY });
|
|
96
|
+
let malformed = inspected.corruptions;
|
|
97
|
+
const matching = inspected.records.flatMap((record: any, ordinal: number) => {
|
|
98
|
+
if (record?.taskId !== input.task.taskId || record?.taskRunId !== input.task.taskRunId || record?.sessionId !== input.task.sessionId) return [];
|
|
99
|
+
if (record.schemaVersion !== 1 || !timestamp(record.recordedAt)) { malformed += 1; return []; }
|
|
100
|
+
if (record.event === "session_compact") {
|
|
101
|
+
if (typeof record.willRetry !== "boolean" || typeof record.fromExtension !== "boolean") { malformed += 1; return []; }
|
|
102
|
+
return [{ ordinal, recordedAt: record.recordedAt, kind: "context-compaction", title: "Context đã được compact",
|
|
103
|
+
detail: display(record.reason, 180) || null, toolName: null, originalChars: null, originalLines: null, captureCount: null,
|
|
104
|
+
willRetry: record.willRetry, fromExtension: record.fromExtension }];
|
|
105
|
+
}
|
|
106
|
+
if (record.event === "tool_result" && record.compacted === true) {
|
|
107
|
+
const captureCount = boundedInteger(record.compactedCaptures, 10_000);
|
|
108
|
+
if (captureCount === null || captureCount < 1) { malformed += 1; return []; }
|
|
109
|
+
return [{ ordinal, recordedAt: record.recordedAt, kind: "tool-result-compaction", title: "Tool result đã được rút gọn",
|
|
110
|
+
detail: null, toolName: display(record.toolName, 120) || "tool", originalChars: boundedInteger(record.outputChars),
|
|
111
|
+
originalLines: boundedInteger(record.outputLines), captureCount, willRetry: null, fromExtension: null }];
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
});
|
|
115
|
+
const projected = matching.map((event: any, index: number) => {
|
|
116
|
+
const fact = { ...event }; delete fact.ordinal;
|
|
117
|
+
const digest = createHash("sha256").update(JSON.stringify({ taskRunId: input.task.taskRunId, ordinal: event.ordinal, fact })).digest("hex");
|
|
118
|
+
return { eventRef: opaque("compaction", `${input.task.taskRunId}\0${digest}`), evidenceRef: opaque("telemetry", digest),
|
|
119
|
+
sequence: index + 1, ...fact };
|
|
120
|
+
});
|
|
121
|
+
const events = projected.slice(-MAX_EVENTS);
|
|
122
|
+
const contextCompactions = projected.filter((event: any) => event.kind === "context-compaction").length;
|
|
123
|
+
const toolResultCompactions = projected.filter((event: any) => event.kind === "tool-result-compaction").length;
|
|
124
|
+
const compactedToolResults = projected.reduce((sum: number, event: any) => sum + (event.captureCount ?? 0), 0);
|
|
125
|
+
const warnings: Array<{ code: string; count: number; message: string }> = [];
|
|
126
|
+
if (!inspected.exists) warnings.push({ code: "telemetry-missing", count: 1, message: "No bounded compaction telemetry was found for this project." });
|
|
127
|
+
if (malformed > 0) warnings.push({ code: "telemetry-corrupt", count: malformed, message: `${malformed} telemetry record(s) could not be validated.` });
|
|
128
|
+
if (inspected.recoverableTailBytes > 0) warnings.push({ code: "telemetry-incomplete-tail", count: inspected.recoverableTailBytes,
|
|
129
|
+
message: "An incomplete telemetry tail may omit the latest compaction fact." });
|
|
130
|
+
if (inspected.inputTruncated) warnings.push({ code: "telemetry-input-truncated", count: 1,
|
|
131
|
+
message: "Older telemetry is outside the bounded inspection window." });
|
|
132
|
+
if (projected.length > events.length) warnings.push({ code: "history-truncated", count: projected.length - events.length,
|
|
133
|
+
message: `${projected.length - events.length} older compaction event(s) are outside this bounded page.` });
|
|
134
|
+
const recovery = recoverySummary(input.cwd, input.task);
|
|
135
|
+
const completeness = !inspected.exists ? "missing" : malformed || inspected.recoverableTailBytes || inspected.inputTruncated ? "partial" : "complete";
|
|
136
|
+
const summary = { contextCompactions, toolResultCompactions, compactedToolResults };
|
|
137
|
+
const historyRevision = `recovery-history.${createHash("sha256").update(JSON.stringify({ events, summary, recovery, completeness })).digest("hex")}`;
|
|
138
|
+
return {
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
version: "piagent-webui-recovery-history-v1",
|
|
141
|
+
generatedAt,
|
|
142
|
+
identity: structuredClone(input.identity),
|
|
143
|
+
runRef,
|
|
144
|
+
state: "ready",
|
|
145
|
+
historyRevision,
|
|
146
|
+
completeness,
|
|
147
|
+
summary,
|
|
148
|
+
recovery,
|
|
149
|
+
retainedContent: { access: "omitted", exposed: false, reasonCode: "protected-runtime-evidence" },
|
|
150
|
+
events,
|
|
151
|
+
page: { total: projected.length, returned: events.length, truncated: projected.length > events.length },
|
|
152
|
+
warnings,
|
|
153
|
+
health: warnings.length
|
|
154
|
+
? { state: "degraded", reasonCode: "recovery-history-warning", message: "Some compaction or recovery facts are unavailable or omitted." }
|
|
155
|
+
: { state: "ok", reasonCode: null, message: null }
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
return unavailable(input.identity, runRef, generatedAt);
|
|
159
|
+
}
|
|
160
|
+
}
|