@piagent/platform 1.3.0 → 1.4.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/CHANGELOG.md +56 -3
- package/README.md +3 -3
- 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 +10 -2
- 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/herdr-workflow.md +1 -0
- package/docs/mcp-and-tools.md +14 -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 +1 -0
- package/docs/quality-benchmark.md +1 -0
- package/docs/quickstart-vietnamese.md +3 -2
- package/docs/release-install-policy.md +11 -7
- package/docs/runtime-harness-standard.md +1 -0
- 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 +1 -0
- 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 +1 -0
- package/docs/vercel-docs-site.md +1 -0
- package/docs/workflow-recipes.md +1 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +1 -1
- package/docs-site/workflows.html +1 -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 +30 -3
- 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 +280 -0
- package/packages/piagent-core/capabilities/runtime-integrity.js +153 -1
- package/packages/piagent-core/extensions/document-intake.ts +24 -10
- package/packages/piagent-core/extensions/piagent-guard.ts +90 -73
- package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
- package/packages/piagent-core/extensions/state-retention.js +77 -44
- 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-server-catalog.js +5 -5
- 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 +5 -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 +68 -4
- 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 +13 -14
- 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-webui/dist/client/assets/index-DjMntaK5.js +172 -0
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -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 +408 -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/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 +204 -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 +485 -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 +110 -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/check-architecture.mjs +1 -0
- package/scripts/check-doc-languages.mjs +19 -2
- package/scripts/check-third-party-neutrality.mjs +43 -0
- package/scripts/install-global.sh +1 -1
- package/scripts/piagent-cli.mjs +17 -3
- package/scripts/piagent-dashboard.mjs +208 -0
- package/scripts/piagent-webui-launcher.mjs +121 -0
- package/scripts/typescript-loader.mjs +26 -1
- package/scripts/verify-local.sh +253 -0
- package/scripts/verify-release-identity.mjs +4 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { activeSessionTask } from "../../extensions/task-state.js";
|
|
4
|
+
import { readTaskJournal, transactTaskJournal } from "../../extensions/task-journal.js";
|
|
5
|
+
|
|
6
|
+
const CONTROL_FACTS = new Set([
|
|
7
|
+
"task-control.stop-requested", "task-control.stop-settled", "task-control.pause-requested", "task-control.paused",
|
|
8
|
+
"task-control.pause-cancelled", "task-control.resume-requested", "task-control.resumed", "task-control.resume-rejected"
|
|
9
|
+
]);
|
|
10
|
+
const REF = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
11
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
12
|
+
export type DurableControlState = "active" | "pause-requested" | "paused" | "terminal" | "unknown";
|
|
13
|
+
type Task = { taskId: string; taskRunId: string; sessionId: string; trace: { outcome: string } };
|
|
14
|
+
type JournalEvent = { sequence: number; hash: string; eventType: string; taskRunId?: string; taskId?: string; sessionId?: string;
|
|
15
|
+
recordedAt: string; data?: Record<string, any> };
|
|
16
|
+
export type TaskControlProjection = { state: DurableControlState; controlRevision: string; pauseEpoch: number; dispatchBlocked: boolean;
|
|
17
|
+
stopPending: boolean; journalHead: string | null; reasonCode: string | null; sequence: number };
|
|
18
|
+
export type TaskControlTransition = { cwd: string; task: Task; runtimeInstanceId: string; commandId: string; idempotencyKeyDigest: string;
|
|
19
|
+
actionDigest: string; fact: string; action: "stop" | "pause" | "resume"; expectedControlRevision: string; expectedStates: DurableControlState[];
|
|
20
|
+
toState: DurableControlState; agentOperationId?: string | null; resultCode: string; reasonCode?: string | null; pauseEpoch?: number;
|
|
21
|
+
preWorkingTreeDigest?: string | null; postWorkingTreeDigest?: string | null };
|
|
22
|
+
|
|
23
|
+
function hash(value: unknown): string { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
|
|
24
|
+
function revision(taskRunId: string, state: DurableControlState, event?: JournalEvent): string {
|
|
25
|
+
return `control-rev.${hash([taskRunId, state, event?.sequence ?? 0, event?.hash ?? null])}`;
|
|
26
|
+
}
|
|
27
|
+
function evidence(event?: JournalEvent): string | null { return event?.hash ? `journal.${event.hash}` : null; }
|
|
28
|
+
function taskEvents(events: JournalEvent[], task: Task): JournalEvent[] {
|
|
29
|
+
return events.filter((event) => event.taskRunId === task.taskRunId && event.taskId === task.taskId && event.sessionId === task.sessionId);
|
|
30
|
+
}
|
|
31
|
+
function project(events: JournalEvent[], task: Task, corruptions: string[] = []): TaskControlProjection {
|
|
32
|
+
const relevant = taskEvents(events, task).filter((event) => CONTROL_FACTS.has(event.eventType));
|
|
33
|
+
const latest = relevant.at(-1), sequence = latest?.sequence ?? 0;
|
|
34
|
+
if (task.trace.outcome !== "pending") return { state: "terminal", controlRevision: revision(task.taskRunId, "terminal", latest), pauseEpoch: 0,
|
|
35
|
+
dispatchBlocked: true, stopPending: false, journalHead: evidence(latest), reasonCode: null, sequence };
|
|
36
|
+
if (corruptions.length) return { state: "unknown", controlRevision: revision(task.taskRunId, "unknown", latest), pauseEpoch: 0,
|
|
37
|
+
dispatchBlocked: true, stopPending: false, journalHead: evidence(latest), reasonCode: "task-journal-corrupt", sequence };
|
|
38
|
+
let state: DurableControlState = "active", pauseEpoch = 0;
|
|
39
|
+
const pendingStops = new Set<string>();
|
|
40
|
+
for (const event of relevant) {
|
|
41
|
+
const data = event.data ?? {}, commandId = String(data.commandId ?? "");
|
|
42
|
+
if (event.eventType === "task-control.pause-requested") { state = "pause-requested"; pauseEpoch = Number(data.pauseEpoch) || pauseEpoch + 1; }
|
|
43
|
+
else if (event.eventType === "task-control.paused") { state = "paused"; pauseEpoch = Number(data.pauseEpoch) || pauseEpoch; }
|
|
44
|
+
else if (event.eventType === "task-control.pause-cancelled" || event.eventType === "task-control.resumed") state = "active";
|
|
45
|
+
if (event.eventType === "task-control.stop-requested" && commandId) pendingStops.add(commandId);
|
|
46
|
+
if (event.eventType === "task-control.stop-settled" && commandId) pendingStops.delete(commandId);
|
|
47
|
+
}
|
|
48
|
+
const stopPending = pendingStops.size > 0;
|
|
49
|
+
return { state, controlRevision: revision(task.taskRunId, state, latest), pauseEpoch, dispatchBlocked: state !== "active",
|
|
50
|
+
stopPending, journalHead: evidence(latest), reasonCode: null, sequence };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function inspectTaskControlState(cwd: string, task: Task): TaskControlProjection {
|
|
54
|
+
const journal = readTaskJournal(cwd, { taskRunId: task.taskRunId, sessionId: task.sessionId });
|
|
55
|
+
return project(journal.events as JournalEvent[], task, journal.corruptions);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function appendTaskControlTransition(input: TaskControlTransition): { ok: boolean; duplicate: boolean; reasonCode: string | null;
|
|
59
|
+
before: TaskControlProjection; after: TaskControlProjection; evidenceRef: string | null } {
|
|
60
|
+
let before = inspectTaskControlState(input.cwd, input.task), duplicate = false, failure: string | null = null;
|
|
61
|
+
let record: JournalEvent | undefined;
|
|
62
|
+
const outcome = transactTaskJournal(input.cwd, ({ events }: { events: JournalEvent[] }) => {
|
|
63
|
+
const currentTask = activeSessionTask(input.cwd, input.task.sessionId) as Task | undefined;
|
|
64
|
+
before = project(events, input.task);
|
|
65
|
+
if (!currentTask || currentTask.taskId !== input.task.taskId || currentTask.taskRunId !== input.task.taskRunId) {
|
|
66
|
+
failure = "task-identity-mismatch"; return { result: null };
|
|
67
|
+
}
|
|
68
|
+
if (currentTask.trace.outcome !== "pending") { failure = "task-terminal"; return { result: null }; }
|
|
69
|
+
if (before.controlRevision !== input.expectedControlRevision) { failure = "stale-control-revision"; return { result: null }; }
|
|
70
|
+
if (!input.expectedStates.includes(before.state)) { failure = `control-state-${before.state}`; return { result: null }; }
|
|
71
|
+
const prior = taskEvents(events, input.task).find((event) => event.eventType === input.fact
|
|
72
|
+
&& (event.data?.commandId === input.commandId || event.data?.idempotencyKeyDigest === input.idempotencyKeyDigest));
|
|
73
|
+
if (prior) {
|
|
74
|
+
if (prior.data?.idempotencyKeyDigest !== input.idempotencyKeyDigest || prior.data?.actionDigest !== input.actionDigest) failure = "idempotency-payload-mismatch";
|
|
75
|
+
else { duplicate = true; record = prior; }
|
|
76
|
+
return { result: null };
|
|
77
|
+
}
|
|
78
|
+
const commandConflict = taskEvents(events, input.task).find((event) => event.data?.commandId === input.commandId
|
|
79
|
+
&& (event.data?.idempotencyKeyDigest !== input.idempotencyKeyDigest || event.data?.actionDigest !== input.actionDigest));
|
|
80
|
+
if (commandConflict) { failure = "idempotency-payload-mismatch"; return { result: null }; }
|
|
81
|
+
return { event: { eventType: input.fact, taskRunId: input.task.taskRunId, taskId: input.task.taskId, sessionId: input.task.sessionId,
|
|
82
|
+
idempotencyKey: input.commandId, data: { schemaVersion: 1, runtimeInstanceId: input.runtimeInstanceId, commandId: input.commandId,
|
|
83
|
+
idempotencyKeyDigest: input.idempotencyKeyDigest, actionDigest: input.actionDigest, action: input.action,
|
|
84
|
+
expectedControlRevision: input.expectedControlRevision, fromControlState: before.state, toControlState: input.toState,
|
|
85
|
+
pauseEpoch: input.pauseEpoch ?? before.pauseEpoch, agentOperationId: input.agentOperationId ?? null,
|
|
86
|
+
resultCode: input.resultCode, reasonCode: input.reasonCode ?? null,
|
|
87
|
+
preWorkingTreeDigest: input.preWorkingTreeDigest ?? null, postWorkingTreeDigest: input.postWorkingTreeDigest ?? null } }, result: null };
|
|
88
|
+
}) as unknown as { appended: boolean; record?: JournalEvent };
|
|
89
|
+
if (outcome.appended) record = outcome.record;
|
|
90
|
+
const after = inspectTaskControlState(input.cwd, input.task);
|
|
91
|
+
return { ok: !failure, duplicate, reasonCode: failure, before, after, evidenceRef: evidence(record) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function readTaskControlReceipt(cwd: string, task: Task, binding: { commandId: string; idempotencyKeyDigest: string;
|
|
95
|
+
actionDigest: string }): { receipt: Record<string, unknown>; conflict: boolean; invalid: boolean } | null {
|
|
96
|
+
const journal = readTaskJournal(cwd, { taskRunId: task.taskRunId, sessionId: task.sessionId });
|
|
97
|
+
if (journal.corruptions.length) return null;
|
|
98
|
+
const event = [...journal.events].reverse().find((item: any) => item.eventType === "task-control.command-receipt"
|
|
99
|
+
&& item.taskId === task.taskId && (item.data?.commandId === binding.commandId
|
|
100
|
+
|| item.data?.idempotencyKeyDigest === binding.idempotencyKeyDigest));
|
|
101
|
+
if (!event?.data?.receipt || typeof event.data.receipt !== "object") return null;
|
|
102
|
+
const receipt = event.data.receipt as Record<string, unknown>;
|
|
103
|
+
const invalid = receipt.commandId !== event.data.commandId || receipt.idempotencyKeyDigest !== event.data.idempotencyKeyDigest
|
|
104
|
+
|| receipt.actionDigest !== event.data.actionDigest;
|
|
105
|
+
const conflict = event.data.actionDigest !== binding.actionDigest || event.data.commandId === binding.commandId
|
|
106
|
+
&& event.data.idempotencyKeyDigest !== binding.idempotencyKeyDigest;
|
|
107
|
+
return { receipt: structuredClone(receipt), conflict, invalid };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function recordTaskControlReceipt(cwd: string, task: Task, commandId: string, idempotencyKeyDigest: string,
|
|
111
|
+
actionDigest: string, receipt: Record<string, unknown>): { ok: boolean; duplicate: boolean; evidenceRef: string | null } {
|
|
112
|
+
let duplicate = false, conflict = false, record: JournalEvent | undefined;
|
|
113
|
+
const result = transactTaskJournal(cwd, ({ events }: { events: JournalEvent[] }) => {
|
|
114
|
+
const prior = taskEvents(events, task).find((event) => event.eventType === "task-control.command-receipt"
|
|
115
|
+
&& (event.data?.commandId === commandId || event.data?.idempotencyKeyDigest === idempotencyKeyDigest));
|
|
116
|
+
if (prior) {
|
|
117
|
+
if (prior.data?.idempotencyKeyDigest !== idempotencyKeyDigest || prior.data?.actionDigest !== actionDigest) conflict = true;
|
|
118
|
+
else if (JSON.stringify(prior.data?.receipt) === JSON.stringify(receipt)) { duplicate = true; record = prior; }
|
|
119
|
+
if (conflict || duplicate) return { result: null };
|
|
120
|
+
}
|
|
121
|
+
return { event: { eventType: "task-control.command-receipt", taskRunId: task.taskRunId, taskId: task.taskId, sessionId: task.sessionId,
|
|
122
|
+
idempotencyKey: `${commandId}:${String(receipt.phase ?? "receipt")}:${String(receipt.resultCode ?? "result")}`,
|
|
123
|
+
data: { schemaVersion: 1, commandId, idempotencyKeyDigest, actionDigest, receipt } }, result: null };
|
|
124
|
+
}) as unknown as { appended: boolean; record?: JournalEvent };
|
|
125
|
+
if (result.appended) record = result.record;
|
|
126
|
+
return { ok: !conflict, duplicate, evidenceRef: evidence(record) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function validTaskControlBinding(value: { commandId: string; idempotencyKeyDigest: string; actionDigest: string }): boolean {
|
|
130
|
+
return REF.test(value.commandId) && DIGEST.test(value.idempotencyKeyDigest) && DIGEST.test(value.actionDigest);
|
|
131
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { redactSensitiveText } from "../../extensions/redaction-core.js";
|
|
4
|
+
import { readHandoffProjection } from "../recovery/handoff-projection.ts";
|
|
5
|
+
import { inspectTaskResumeState } from "../recovery/resume-state.ts";
|
|
6
|
+
import { inspectBoundedContextTelemetry } from "./context-telemetry-inspection.ts";
|
|
7
|
+
import { taskRunOpaqueRef } from "./task-run-index.ts";
|
|
8
|
+
|
|
9
|
+
const MAX_EVENTS = 100;
|
|
10
|
+
|
|
11
|
+
type Task = { taskId: string; taskRunId: string; sessionId: string; [key: string]: any };
|
|
12
|
+
type Identity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string; taskRunId: string;
|
|
13
|
+
agentOperationId: null; toolCallId: null };
|
|
14
|
+
|
|
15
|
+
function opaque(prefix: string, value: string): string {
|
|
16
|
+
return `${prefix}.${createHash("sha256").update(`piagent-webui-handoff-history-v1\0${value}`).digest("hex").slice(0, 48)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function display(value: unknown, maximum = 240): string {
|
|
20
|
+
return redactSensitiveText(String(value ?? "")).text.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function timestamp(value: unknown): value is string {
|
|
24
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
|
|
25
|
+
&& Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nextAction(cwd: string, task: Task) {
|
|
29
|
+
const resume = inspectTaskResumeState(cwd, task as any, task.sessionId), next = resume.reconstruction.nextAction;
|
|
30
|
+
return { action: next.action, stepRef: next.stepId ? opaque("step", `${task.taskRunId}\0${next.stepId}`) : null,
|
|
31
|
+
reason: display(next.reason, 500) || "No authoritative next-action reason is available.", exactCommandCount: next.exactCommands.length,
|
|
32
|
+
dispatchable: false, enforcementSafe: resume.enforcementSafe };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function currentHandoff(cwd: string, task: Task) {
|
|
36
|
+
const handoff = readHandoffProjection(cwd, task.taskRunId); if (!handoff) return null;
|
|
37
|
+
if (handoff.identity.taskId !== task.taskId || handoff.identity.taskRunId !== task.taskRunId) throw new Error("handoff-identity-conflict");
|
|
38
|
+
return { handoffRef: opaque("handoff", `${task.taskRunId}\0${handoff.generatedAt}\0${handoff.tree.currentDigest ?? "none"}`),
|
|
39
|
+
generatedAt: handoff.generatedAt, phase: display(handoff.state.phase, 64) || null, taskOutcome: handoff.state.taskOutcome,
|
|
40
|
+
gateDecision: handoff.state.gateDecision, completionApproved: handoff.state.completionApproved,
|
|
41
|
+
requiredAuthority: handoff.requiredAuthority.kind, treeEvidenceCurrent: handoff.tree.evidenceCurrent,
|
|
42
|
+
latestVerifierMatchesCurrentTree: handoff.tree.latestVerifierMatchesCurrentTree,
|
|
43
|
+
changedFileCount: handoff.changedFiles.current.length, missingCount: handoff.state.missing.length,
|
|
44
|
+
projectedAction: display(handoff.nextSafeAction.action, 80) || "unknown" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unavailable(identity: Identity, runRef: string, generatedAt: string, reasonCode: string) {
|
|
48
|
+
return { schemaVersion: 1, version: "piagent-webui-handoff-history-v1", generatedAt, identity: structuredClone(identity), runRef,
|
|
49
|
+
state: "unavailable", historyRevision: null, completeness: "unknown", current: null,
|
|
50
|
+
nextAction: { action: "unknown", stepRef: null, reason: "Handoff authority is unavailable.", exactCommandCount: 0,
|
|
51
|
+
dispatchable: false, enforcementSafe: false }, events: [], page: { total: 0, returned: 0, truncated: false }, warnings: [],
|
|
52
|
+
health: { state: "error", reasonCode, message: "Handoff history is unavailable." } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function projectTaskHandoffHistory(input: { cwd: string; task: Task; identity: Identity; generatedAt?: string }): Record<string, any> {
|
|
56
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString(), runRef = taskRunOpaqueRef(input.task.taskRunId);
|
|
57
|
+
try {
|
|
58
|
+
const inspected = inspectBoundedContextTelemetry(input.cwd, { limit: 5_000 });
|
|
59
|
+
let malformed = inspected.corruptions;
|
|
60
|
+
const matching = inspected.records.flatMap((record: any, ordinal: number) => {
|
|
61
|
+
if (record?.event !== "handoff_projection_written" || record.taskId !== input.task.taskId
|
|
62
|
+
|| record.taskRunId !== input.task.taskRunId || record.sessionId !== input.task.sessionId) return [];
|
|
63
|
+
if (record.schemaVersion !== 1 || !timestamp(record.recordedAt) || typeof record.completionApproved !== "boolean") { malformed += 1; return []; }
|
|
64
|
+
const action = display(record.recoveryAction, 80) || "unknown", phase = display(record.phase, 64) || null;
|
|
65
|
+
return [{ ordinal, recordedAt: record.recordedAt, phase, completionApproved: record.completionApproved, projectedAction: action }];
|
|
66
|
+
});
|
|
67
|
+
const projected = matching.map((event: any, index: number) => {
|
|
68
|
+
const fact = { ...event }; delete fact.ordinal;
|
|
69
|
+
const digest = createHash("sha256").update(JSON.stringify({ taskRunId: input.task.taskRunId, ordinal: event.ordinal, fact })).digest("hex");
|
|
70
|
+
return { eventRef: opaque("handoff-event", digest), evidenceRef: opaque("telemetry", digest), sequence: index + 1, ...fact };
|
|
71
|
+
});
|
|
72
|
+
const events = projected.slice(-MAX_EVENTS), current = currentHandoff(input.cwd, input.task), next = nextAction(input.cwd, input.task);
|
|
73
|
+
const warnings: Array<{ code: string; count: number; message: string }> = [];
|
|
74
|
+
if (!inspected.exists) warnings.push({ code: "telemetry-missing", count: 1, message: "No bounded handoff telemetry was found for this project." });
|
|
75
|
+
if (malformed > 0) warnings.push({ code: "telemetry-corrupt", count: malformed, message: `${malformed} telemetry record(s) could not be validated.` });
|
|
76
|
+
if (inspected.recoverableTailBytes > 0) warnings.push({ code: "telemetry-incomplete-tail", count: inspected.recoverableTailBytes,
|
|
77
|
+
message: "An incomplete telemetry tail may omit the latest handoff fact." });
|
|
78
|
+
if (inspected.inputTruncated) warnings.push({ code: "telemetry-input-truncated", count: 1, message: "Older handoff telemetry is outside the bounded inspection window." });
|
|
79
|
+
if (projected.length > events.length) warnings.push({ code: "history-truncated", count: projected.length - events.length,
|
|
80
|
+
message: `${projected.length - events.length} older handoff event(s) are outside this bounded page.` });
|
|
81
|
+
if (!current) warnings.push({ code: "current-handoff-missing", count: 1, message: "No current authoritative handoff projection was found for this run." });
|
|
82
|
+
if (current && projected.length === 0) warnings.push({ code: "history-snapshot-only", count: 1, message: "Only the latest authoritative handoff snapshot is available." });
|
|
83
|
+
const incomplete = malformed > 0 || inspected.recoverableTailBytes > 0 || inspected.inputTruncated;
|
|
84
|
+
const completeness = incomplete || !current && projected.length > 0 ? "partial" : current && projected.length === 0 ? "snapshot-only"
|
|
85
|
+
: !current && projected.length === 0 ? "missing" : "complete";
|
|
86
|
+
const historyRevision = `handoff-history.${createHash("sha256").update(JSON.stringify({ current, next, events, completeness })).digest("hex")}`;
|
|
87
|
+
return { schemaVersion: 1, version: "piagent-webui-handoff-history-v1", generatedAt, identity: structuredClone(input.identity), runRef,
|
|
88
|
+
state: "ready", historyRevision, completeness, current, nextAction: next, events,
|
|
89
|
+
page: { total: projected.length, returned: events.length, truncated: projected.length > events.length }, warnings,
|
|
90
|
+
health: warnings.length ? { state: "degraded", reasonCode: "handoff-history-warning", message: "Some handoff facts are unavailable or omitted." }
|
|
91
|
+
: { state: "ok", reasonCode: null, message: null } };
|
|
92
|
+
} catch {
|
|
93
|
+
return unavailable(input.identity, runRef, generatedAt, "handoff-history-invalid");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeMutationRepoPath,
|
|
3
|
+
type MutationProvenanceRecord
|
|
4
|
+
} from "./mutation-provenance-contract.ts";
|
|
5
|
+
import { readMutationProvenance } from "./mutation-provenance-store.ts";
|
|
6
|
+
import { taskBaselineManifestRef } from "./source-evidence-store.ts";
|
|
7
|
+
import type { TaskBaselineEntry, TaskBaselineManifest } from "./source-evidence-contract.ts";
|
|
8
|
+
|
|
9
|
+
export type SourceProvenance = {
|
|
10
|
+
classification: "runtime-observed-agent" | "post-baseline-unattributed" | "mixed";
|
|
11
|
+
evidence: "exact" | "unavailable";
|
|
12
|
+
baselineEvidenceRef: string | null;
|
|
13
|
+
mutationEvidenceRefs: string[];
|
|
14
|
+
reasonCode: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ExactState = { contentDigest: string; evidenceRefs: string[] };
|
|
18
|
+
|
|
19
|
+
function paths(record: MutationProvenanceRecord): Array<{ path: string; index: number }> {
|
|
20
|
+
const result: Array<{ path: string; index: number }> = [];
|
|
21
|
+
for (let index = 0; index < record.changes.length; index += 1) {
|
|
22
|
+
const decoded = decodeMutationRepoPath(record.changes[index]);
|
|
23
|
+
if (decoded) result.push({ path: decoded, index });
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function taskProvenanceResolver(projectRoot: string, manifest: TaskBaselineManifest) {
|
|
29
|
+
const ledger = readMutationProvenance(projectRoot, manifest.taskRunId);
|
|
30
|
+
const exact = new Map<string, ExactState>();
|
|
31
|
+
if (ledger.corruptions.length === 0) {
|
|
32
|
+
for (const record of ledger.records) {
|
|
33
|
+
for (const item of paths(record)) {
|
|
34
|
+
const change = record.changes[item.index];
|
|
35
|
+
const current = exact.get(item.path);
|
|
36
|
+
if (record.evidenceMode === "exact-runtime" && change.afterContentDigest) {
|
|
37
|
+
exact.set(item.path, {
|
|
38
|
+
contentDigest: change.afterContentDigest,
|
|
39
|
+
evidenceRefs: [...new Set([...(current?.evidenceRefs ?? []), record.evidenceRef])].slice(-64)
|
|
40
|
+
});
|
|
41
|
+
} else if (!(change.effect === "content-preserved" && current
|
|
42
|
+
&& change.afterContentDigest === current.contentDigest)) {
|
|
43
|
+
exact.delete(item.path);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return (projectPath: string, currentContentDigest: string | null, baselineEntry?: TaskBaselineEntry): SourceProvenance => {
|
|
49
|
+
const state = exact.get(projectPath);
|
|
50
|
+
if (!state || !currentContentDigest || state.contentDigest !== currentContentDigest) {
|
|
51
|
+
return {
|
|
52
|
+
classification: "post-baseline-unattributed", evidence: "unavailable", baselineEvidenceRef: null,
|
|
53
|
+
mutationEvidenceRefs: [], reasonCode: ledger.corruptions.length ? "provenance-ledger-corrupt" : "provenance-evidence-unavailable"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return baselineEntry
|
|
57
|
+
? { classification: "mixed", evidence: "exact", baselineEvidenceRef: taskBaselineManifestRef(manifest), mutationEvidenceRefs: state.evidenceRefs, reasonCode: null }
|
|
58
|
+
: { classification: "runtime-observed-agent", evidence: "exact", baselineEvidenceRef: null, mutationEvidenceRefs: state.evidenceRefs, reasonCode: null };
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { redactSensitiveText } from "../../extensions/redaction-core.js";
|
|
4
|
+
import { readTaskJournal, replayTaskCheckpoints, taskRecoveryDecision } from "../../extensions/task-journal.js";
|
|
5
|
+
import { taskRunOpaqueRef } from "./task-run-index.ts";
|
|
6
|
+
|
|
7
|
+
const MAX_EVENTS = 300;
|
|
8
|
+
const KINDS: Record<string, { kind: string; state: string; title: string }> = {
|
|
9
|
+
"contract-written": { kind: "task-written", state: "observed", title: "Task Contract được ghi" },
|
|
10
|
+
"session-bound": { kind: "session-bound", state: "observed", title: "Session được bind vào task" },
|
|
11
|
+
checkpoint: { kind: "checkpoint", state: "observed", title: "Checkpoint được ghi" },
|
|
12
|
+
"task-control.pause-requested": { kind: "pause-requested", state: "requested", title: "Yêu cầu tạm dừng" },
|
|
13
|
+
"task-control.paused": { kind: "paused", state: "settled", title: "Task đã tạm dừng" },
|
|
14
|
+
"task-control.pause-cancelled": { kind: "pause-cancelled", state: "settled", title: "Yêu cầu tạm dừng đã hủy" },
|
|
15
|
+
"task-control.resume-requested": { kind: "resume-requested", state: "requested", title: "Yêu cầu tiếp tục" },
|
|
16
|
+
"task-control.resumed": { kind: "resumed", state: "settled", title: "Task đã tiếp tục" },
|
|
17
|
+
"task-control.resume-rejected": { kind: "resume-rejected", state: "failed", title: "Yêu cầu tiếp tục bị từ chối" },
|
|
18
|
+
"task-control.stop-requested": { kind: "stop-requested", state: "requested", title: "Yêu cầu dừng operation" },
|
|
19
|
+
"task-control.stop-settled": { kind: "stop-settled", state: "settled", title: "Operation đã dừng" },
|
|
20
|
+
"continuation-consumed": { kind: "continuation", state: "observed", title: "Continuation budget đã dùng" },
|
|
21
|
+
"digest-migrated": { kind: "digest-migrated", state: "observed", title: "Evidence digest đã migrate" }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Task = { taskId: string; taskRunId: string; sessionId: string; trace: { outcome: string }; [key: string]: any };
|
|
25
|
+
type Identity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string; taskRunId: string;
|
|
26
|
+
agentOperationId: null; toolCallId: null };
|
|
27
|
+
|
|
28
|
+
function opaque(prefix: string, value: string): string {
|
|
29
|
+
return `${prefix}.${createHash("sha256").update(`piagent-webui-task-timeline-v1\0${value}`).digest("hex").slice(0, 48)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function display(value: unknown, maximum = 160): string {
|
|
33
|
+
return redactSensitiveText(String(value ?? "")).text.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function eventDetail(event: any): string | null {
|
|
37
|
+
const data = event.data ?? {};
|
|
38
|
+
if (event.eventType === "checkpoint") return display(`${data.phase ?? "unknown"} · ${data.status ?? "unknown"}`) || null;
|
|
39
|
+
if (event.eventType.startsWith("task-control.")) return display(data.resultCode ?? data.reasonCode ?? "") || null;
|
|
40
|
+
if (event.eventType === "continuation-consumed") return display(`${data.classification ?? "continuation"} · ${data.action ?? "unknown"}`) || null;
|
|
41
|
+
if (event.eventType === "digest-migrated") return display(`${data.disposition ?? "migration"} · ${data.reasonCode ?? "unknown"}`) || null;
|
|
42
|
+
if (event.eventType === "contract-written") return display(data.outcome ?? "") || null;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function projectEvent(event: any, task: Task) {
|
|
47
|
+
const descriptor = KINDS[event.eventType]; if (!descriptor) return null;
|
|
48
|
+
const checkpointRef = event.eventType === "checkpoint" ? opaque("checkpoint", `${task.taskRunId}\0${event.checkpointId ?? "checkpoint"}`) : null;
|
|
49
|
+
return { eventRef: opaque("event", `${task.taskRunId}\0${event.sequence}\0${event.hash}`), evidenceRef: `journal.${event.hash}`,
|
|
50
|
+
sequence: event.sequence, recordedAt: event.recordedAt, ...descriptor, detail: eventDetail(event), checkpointRef };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unavailable(identity: Identity, runRef: string, generatedAt: string, corruptions: number) {
|
|
54
|
+
return { schemaVersion: 1, version: "piagent-webui-task-timeline-v1", generatedAt, identity, runRef, state: "unavailable",
|
|
55
|
+
timelineRevision: null, continuity: { crashEvidence: "unknown", recoveryDecision: "unknown", latestCheckpointRef: null,
|
|
56
|
+
reasonCode: "task-journal-corrupt" }, events: [], page: { total: 0, returned: 0, truncated: false },
|
|
57
|
+
warnings: [{ code: "journal-corrupt", count: Math.max(1, corruptions), message: "Task journal could not be validated." }],
|
|
58
|
+
health: { state: "error", reasonCode: "task-journal-corrupt", message: "Timeline authority is unavailable." } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function projectTaskRecoveryTimeline(input: { cwd: string; task: Task; identity: Identity; generatedAt?: string }): Record<string, unknown> {
|
|
62
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString(), runRef = taskRunOpaqueRef(input.task.taskRunId);
|
|
63
|
+
const journal = readTaskJournal(input.cwd, { taskRunId: input.task.taskRunId, sessionId: input.task.sessionId,
|
|
64
|
+
limit: 1000, maximumBytes: 32 * 1024 * 1024 });
|
|
65
|
+
if (journal.corruptions.length) return unavailable(structuredClone(input.identity), runRef, generatedAt, journal.corruptions.length);
|
|
66
|
+
const matching = journal.events.filter((event: any) => event.taskId === input.task.taskId && event.taskRunId === input.task.taskRunId
|
|
67
|
+
&& event.sessionId === input.task.sessionId);
|
|
68
|
+
const projected = matching.map((event: any) => projectEvent(event, input.task)).filter(Boolean) as any[];
|
|
69
|
+
const events = projected.slice(-MAX_EVENTS), replay = replayTaskCheckpoints(input.cwd, input.task.taskRunId, input.task);
|
|
70
|
+
if (replay.corruptions.length) return unavailable(structuredClone(input.identity), runRef, generatedAt, replay.corruptions.length);
|
|
71
|
+
const recovery = taskRecoveryDecision(input.task, replay), latest = replay.checkpoints.at(-1);
|
|
72
|
+
const tailBytes = Number(journal.recoverableTailBytes ?? 0), warnings = [] as Array<{ code: string; count: number; message: string }>;
|
|
73
|
+
if (tailBytes > 0) warnings.push({ code: "recoverable-tail", count: tailBytes, message: "An incomplete journal tail indicates a possible interrupted write." });
|
|
74
|
+
else warnings.push({ code: "crash-not-observed", count: 1, message: "No explicit crash fact was recorded; crash state remains unknown." });
|
|
75
|
+
if (projected.length > events.length) warnings.push({ code: "timeline-truncated", count: projected.length - events.length,
|
|
76
|
+
message: `${projected.length - events.length} older timeline event(s) are outside this bounded page.` });
|
|
77
|
+
if (journal.inputTruncated) warnings.push({ code: "journal-input-truncated", count: Math.max(1, Math.min(100_000, journal.omittedEvents ?? 0)),
|
|
78
|
+
message: "Older task journal records are outside the bounded inspection window." });
|
|
79
|
+
const continuity = { crashEvidence: tailBytes > 0 ? "possible-interruption" : "unknown", recoveryDecision: recovery.decision,
|
|
80
|
+
latestCheckpointRef: latest ? opaque("checkpoint", `${input.task.taskRunId}\0${latest.checkpointId ?? "checkpoint"}`) : null,
|
|
81
|
+
reasonCode: tailBytes > 0 ? "incomplete-journal-tail" : "no-explicit-crash-evidence" };
|
|
82
|
+
const timelineRevision = `timeline.${createHash("sha256").update(JSON.stringify({ events, continuity, head: journal.head?.hash ?? null })).digest("hex")}`;
|
|
83
|
+
return { schemaVersion: 1, version: "piagent-webui-task-timeline-v1", generatedAt, identity: structuredClone(input.identity), runRef, state: "ready",
|
|
84
|
+
timelineRevision, continuity, events, page: { total: projected.length, returned: events.length,
|
|
85
|
+
truncated: projected.length > events.length || journal.inputTruncated }, warnings,
|
|
86
|
+
health: warnings.length ? { state: "degraded", reasonCode: "timeline-warning", message: "Some continuity facts remain unknown or omitted." }
|
|
87
|
+
: { state: "ok", reasonCode: null, message: null } };
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { redactSensitiveText } from "../../extensions/redaction-core.js";
|
|
4
|
+
import { activeSessionTask, listTaskContracts, taskStateMigrationStatus } from "../../extensions/task-state.js";
|
|
5
|
+
|
|
6
|
+
const MAX_RUNS = 200;
|
|
7
|
+
const TERMINAL = new Set(["completed", "blocked", "partial", "failed"]);
|
|
8
|
+
|
|
9
|
+
type Identity = {
|
|
10
|
+
projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string | null; taskRunId: string | null;
|
|
11
|
+
agentOperationId: null; toolCallId: null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function ref(prefix: string, value: string): string {
|
|
15
|
+
return `${prefix}.${createHash("sha256").update(`piagent-webui-task-index-v1\0${value}`).digest("hex").slice(0, 48)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function taskRunOpaqueRef(taskRunId: string): string { return ref("run", taskRunId); }
|
|
19
|
+
export function resolveTaskRunRef(cwd: string, runRef: string): any | undefined {
|
|
20
|
+
return listTaskContracts(cwd).find((task: any) => taskRunOpaqueRef(task.taskRunId) === runRef);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function display(value: unknown, maximum: number): string {
|
|
24
|
+
return redactSensitiveText(String(value ?? "")).text.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function progress(task: any): { completed: number; total: number } {
|
|
28
|
+
const steps = Array.isArray(task.workPlan) ? task.workPlan.slice(0, 12) : [];
|
|
29
|
+
return { completed: steps.filter((step: any) => ["done", "skipped"].includes(step?.status)).length, total: steps.length };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sessionLabel(task: any, isCurrentSession: boolean): string | null {
|
|
33
|
+
const value = display(task.sessionName, 160), rawId = String(task.sessionId ?? "");
|
|
34
|
+
if (!value) return null;
|
|
35
|
+
return rawId && value.includes(rawId) ? isCurrentSession ? "Session hiện tại" : null : value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(task: any, currentSessionId: string, activeTaskRunId: string | null) {
|
|
39
|
+
const outcome = ["pending", "completed", "blocked", "partial", "failed"].includes(task.trace?.outcome) ? task.trace.outcome : "unknown";
|
|
40
|
+
const isCurrentSession = task.sessionId === currentSessionId;
|
|
41
|
+
const isActive = isCurrentSession && outcome === "pending" && task.taskRunId === activeTaskRunId;
|
|
42
|
+
return {
|
|
43
|
+
runRef: taskRunOpaqueRef(task.taskRunId), taskRef: ref("task", task.taskId), taskId: task.taskId, taskRunId: task.taskRunId,
|
|
44
|
+
summary: display(task.summary, 240), sessionLabel: sessionLabel(task, isCurrentSession),
|
|
45
|
+
outcome, terminal: TERMINAL.has(outcome), attempt: task.attempt, maxAttempts: task.maxAttempts,
|
|
46
|
+
changeMode: task.changeMode, riskLane: task.riskLane, createdAt: task.createdAt, updatedAt: task.updatedAt,
|
|
47
|
+
isCurrentSession, isActive, progress: progress(task)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function revision(runs: unknown[], total: number, corrupt: number, legacy: number): string {
|
|
52
|
+
const digest = createHash("sha256").update(JSON.stringify({ runs, total, corrupt, legacy })).digest("hex");
|
|
53
|
+
return `task-index.${digest}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function projectTaskRunIndex(input: {
|
|
57
|
+
cwd: string; identity: Identity; currentSessionId: string; generatedAt?: string; limit?: number;
|
|
58
|
+
}): Record<string, unknown> {
|
|
59
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
60
|
+
try {
|
|
61
|
+
const tasks = listTaskContracts(input.cwd);
|
|
62
|
+
const status = taskStateMigrationStatus(input.cwd);
|
|
63
|
+
const active = activeSessionTask(input.cwd, input.currentSessionId);
|
|
64
|
+
const limit = Math.max(1, Math.min(MAX_RUNS, input.limit ?? 100));
|
|
65
|
+
const rows = tasks.map((task: any) => run(task, input.currentSessionId, active?.taskRunId ?? null))
|
|
66
|
+
.sort((left: any, right: any) => Number(right.isActive) - Number(left.isActive) || Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
67
|
+
const returned = rows.slice(0, limit), warnings = [] as Array<{ code: string; count: number; message: string }>;
|
|
68
|
+
if (status.unreadable.length) warnings.push({ code: "corrupt-task-state", count: status.unreadable.length,
|
|
69
|
+
message: `${status.unreadable.length} task state record(s) could not be validated.` });
|
|
70
|
+
if (status.legacy) warnings.push({ code: "legacy-task-state", count: status.legacy,
|
|
71
|
+
message: `${status.legacy} legacy task state record(s) have reduced evidence.` });
|
|
72
|
+
if (rows.length > returned.length) warnings.push({ code: "task-index-truncated", count: rows.length - returned.length,
|
|
73
|
+
message: `${rows.length - returned.length} older task run(s) are outside this bounded page.` });
|
|
74
|
+
const activeRun = returned.find((item: any) => item.isActive);
|
|
75
|
+
return {
|
|
76
|
+
schemaVersion: 1, version: "piagent-webui-task-index-v1", generatedAt, identity: structuredClone(input.identity), state: "ready",
|
|
77
|
+
indexRevision: revision(returned, rows.length, status.unreadable.length, status.legacy), activeRunRef: activeRun?.runRef ?? null,
|
|
78
|
+
runs: returned, page: { total: rows.length, returned: returned.length, truncated: rows.length > returned.length }, warnings,
|
|
79
|
+
health: warnings.length ? { state: "degraded", reasonCode: "task-index-warning", message: "Some task history is unavailable or omitted." }
|
|
80
|
+
: { state: "ok", reasonCode: null, message: null }
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return { schemaVersion: 1, version: "piagent-webui-task-index-v1", generatedAt, identity: structuredClone(input.identity),
|
|
84
|
+
state: "unavailable", indexRevision: null, activeRunRef: null, runs: [], page: { total: 0, returned: 0, truncated: false },
|
|
85
|
+
warnings: [], health: { state: "unavailable", reasonCode: "task-index-unavailable", message: "Task history is unavailable." } };
|
|
86
|
+
}
|
|
87
|
+
}
|