@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -15
- package/assets/integrations/skills/pappra/SKILL.md +28 -7
- package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
- package/assets/integrations/skills/pappra/references/environment.md +360 -0
- package/assets/integrations/skills/pappra/references/hosts.md +20 -0
- package/assets/integrations/skills/pappra/references/operations.md +36 -5
- package/dist/adapters/pi/recovery-extension.d.ts +65 -0
- package/dist/adapters/pi/recovery-extension.js +238 -0
- package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
- package/dist/adapters/pi/session-transition-extension.js +265 -0
- package/dist/app/automatic-handoff-events.d.ts +26 -0
- package/dist/app/automatic-handoff-events.js +134 -0
- package/dist/app/bounded-file.d.ts +2 -0
- package/dist/app/bounded-file.js +2 -0
- package/dist/app/capability-evidence/claude.d.ts +37 -0
- package/dist/app/capability-evidence/claude.js +206 -0
- package/dist/app/capability-evidence/codex.d.ts +35 -0
- package/dist/app/capability-evidence/codex.js +193 -0
- package/dist/app/capability-evidence/index.d.ts +33 -0
- package/dist/app/capability-evidence/index.js +117 -0
- package/dist/app/capability-evidence/pi.d.ts +73 -0
- package/dist/app/capability-evidence/pi.js +669 -0
- package/dist/app/capability-evidence/shared.d.ts +69 -0
- package/dist/app/capability-evidence/shared.js +195 -0
- package/dist/app/collab-auto-handoff.d.ts +55 -0
- package/dist/app/collab-auto-handoff.js +384 -0
- package/dist/app/collab-bindings.d.ts +57 -0
- package/dist/app/collab-bindings.js +1006 -0
- package/dist/app/collab-bootstrap-transport.d.ts +163 -0
- package/dist/app/collab-bootstrap-transport.js +769 -0
- package/dist/app/collab-bootstrap.d.ts +137 -0
- package/dist/app/collab-bootstrap.js +3455 -0
- package/dist/app/collab-handoff.d.ts +12 -0
- package/dist/app/collab-handoff.js +511 -0
- package/dist/app/collab-messages.d.ts +2 -0
- package/dist/app/collab-messages.js +2 -0
- package/dist/app/collab-notify.d.ts +156 -0
- package/dist/app/collab-notify.js +1208 -0
- package/dist/app/collab-preflight.d.ts +34 -0
- package/dist/app/collab-preflight.js +96 -0
- package/dist/app/collab-recovery-executor.d.ts +161 -0
- package/dist/app/collab-recovery-executor.js +1153 -0
- package/dist/app/collab-recovery-projection.d.ts +103 -0
- package/dist/app/collab-recovery-projection.js +659 -0
- package/dist/app/collab-recovery.d.ts +25 -0
- package/dist/app/collab-recovery.js +429 -0
- package/dist/app/collab-resources.d.ts +16 -0
- package/dist/app/collab-resources.js +278 -0
- package/dist/app/collab-role-events.d.ts +22 -0
- package/dist/app/collab-role-events.js +329 -0
- package/dist/app/collab-role-orchestration.d.ts +53 -0
- package/dist/app/collab-role-orchestration.js +601 -0
- package/dist/app/collab-role-plan.d.ts +81 -0
- package/dist/app/collab-role-plan.js +504 -0
- package/dist/app/collab-role-repair.d.ts +109 -0
- package/dist/app/collab-role-repair.js +821 -0
- package/dist/app/collab-rollover.d.ts +263 -0
- package/dist/app/collab-rollover.js +1112 -0
- package/dist/app/collab-route.d.ts +64 -0
- package/dist/app/collab-route.js +240 -0
- package/dist/app/collab-session-transition.d.ts +221 -0
- package/dist/app/collab-session-transition.js +3334 -0
- package/dist/app/collab-state.d.ts +7 -0
- package/dist/app/collab-state.js +162 -0
- package/dist/app/collab-task-store.d.ts +22 -0
- package/dist/app/collab-task-store.js +204 -0
- package/dist/app/collab-work-execution.d.ts +127 -0
- package/dist/app/collab-work-execution.js +1027 -0
- package/dist/app/collab-work-plan.d.ts +87 -0
- package/dist/app/collab-work-plan.js +437 -0
- package/dist/app/collaboration.d.ts +8 -2
- package/dist/app/collaboration.js +101 -12
- package/dist/app/doctor.js +108 -5
- package/dist/app/host-mcp.js +34 -8
- package/dist/app/host-probe.js +4 -4
- package/dist/app/init.d.ts +58 -4
- package/dist/app/init.js +768 -155
- package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
- package/dist/app/managed-pi-bootstrap-admission.js +104 -0
- package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
- package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
- package/dist/app/managed-pi-delivery.d.ts +8 -0
- package/dist/app/managed-pi-delivery.js +110 -0
- package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
- package/dist/app/managed-pi-handoff-receiver.js +263 -0
- package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
- package/dist/app/managed-pi-handoff-reference.js +60 -0
- package/dist/app/managed-pi-herdr.d.ts +21 -0
- package/dist/app/managed-pi-herdr.js +185 -0
- package/dist/app/managed-pi-initial-admission.d.ts +28 -0
- package/dist/app/managed-pi-initial-admission.js +416 -0
- package/dist/app/managed-pi-initial-launch.d.ts +5 -0
- package/dist/app/managed-pi-initial-launch.js +61 -0
- package/dist/app/managed-pi-native-observation.d.ts +36 -0
- package/dist/app/managed-pi-native-observation.js +191 -0
- package/dist/app/managed-pi-readiness.d.ts +29 -0
- package/dist/app/managed-pi-readiness.js +288 -0
- package/dist/app/managed-pi-repair-stop.d.ts +21 -0
- package/dist/app/managed-pi-repair-stop.js +371 -0
- package/dist/app/managed-pi-resume-admission.d.ts +32 -0
- package/dist/app/managed-pi-resume-admission.js +618 -0
- package/dist/app/managed-pi-retirement.d.ts +28 -0
- package/dist/app/managed-pi-retirement.js +112 -0
- package/dist/app/managed-pi-sdk-control.d.ts +7 -0
- package/dist/app/managed-pi-sdk-control.js +169 -0
- package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
- package/dist/app/managed-pi-sdk-runtime.js +483 -0
- package/dist/app/managed-pi-source-admission.d.ts +14 -0
- package/dist/app/managed-pi-source-admission.js +323 -0
- package/dist/app/managed-pi-transition-host.d.ts +36 -0
- package/dist/app/managed-pi-transition-host.js +292 -0
- package/dist/app/managed-pi.d.ts +54 -0
- package/dist/app/managed-pi.js +248 -0
- package/dist/app/shell-readiness.d.ts +66 -0
- package/dist/app/shell-readiness.js +368 -0
- package/dist/app/task-pin.d.ts +2 -23
- package/dist/app/task-pin.js +2 -113
- package/dist/cli/commands/collaboration-binding.d.ts +13 -0
- package/dist/cli/commands/collaboration-binding.js +118 -0
- package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
- package/dist/cli/commands/collaboration-bootstrap.js +84 -0
- package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
- package/dist/cli/commands/collaboration-capabilities.js +127 -0
- package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
- package/dist/cli/commands/collaboration-handoff.js +102 -0
- package/dist/cli/commands/collaboration-message.d.ts +17 -0
- package/dist/cli/commands/collaboration-message.js +177 -0
- package/dist/cli/commands/collaboration-plan.d.ts +8 -0
- package/dist/cli/commands/collaboration-plan.js +108 -0
- package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
- package/dist/cli/commands/collaboration-preflight.js +167 -0
- package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
- package/dist/cli/commands/collaboration-recovery-run.js +224 -0
- package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
- package/dist/cli/commands/collaboration-recovery.js +104 -0
- package/dist/cli/commands/collaboration-resources.d.ts +11 -0
- package/dist/cli/commands/collaboration-resources.js +96 -0
- package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
- package/dist/cli/commands/collaboration-role-plan.js +69 -0
- package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
- package/dist/cli/commands/collaboration-role-run.js +128 -0
- package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
- package/dist/cli/commands/collaboration-rollover.js +103 -0
- package/dist/cli/commands/collaboration-state.d.ts +10 -0
- package/dist/cli/commands/collaboration-state.js +96 -0
- package/dist/cli/commands/collaboration-transition.d.ts +10 -0
- package/dist/cli/commands/collaboration-transition.js +75 -0
- package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
- package/dist/cli/commands/collaboration-work-run.js +178 -0
- package/dist/cli/commands/collaboration.d.ts +4 -0
- package/dist/cli/commands/collaboration.js +39 -4
- package/dist/cli/commands/continuity.js +2 -6
- package/dist/cli/commands/docs.d.ts +2 -0
- package/dist/cli/commands/docs.js +89 -15
- package/dist/cli/commands/host.d.ts +7 -1
- package/dist/cli/commands/host.js +122 -7
- package/dist/cli/commands/init.d.ts +20 -3
- package/dist/cli/commands/init.js +897 -232
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +11 -5
- package/dist/cli/commands/repair.d.ts +12 -0
- package/dist/cli/commands/repair.js +41 -0
- package/dist/cli/commands/runtime.d.ts +1 -1
- package/dist/cli/commands/runtime.js +139 -38
- package/dist/cli/commands/session.d.ts +7 -0
- package/dist/cli/commands/session.js +82 -0
- package/dist/cli/commands/task.js +19 -5
- package/dist/cli/run.js +866 -24
- package/dist/code/identity.d.ts +7 -0
- package/dist/code/identity.js +7 -3
- package/dist/code/registration.js +7 -0
- package/dist/code/service.d.ts +9 -0
- package/dist/code/service.js +16 -3
- package/dist/code/text.js +19 -7
- package/dist/collab/admission.d.ts +91 -66
- package/dist/collab/admission.js +21 -6
- package/dist/collab/canonical.d.ts +1 -1
- package/dist/collab/canonical.js +44 -3
- package/dist/collab/envelope.d.ts +2 -2
- package/dist/collab/envelope.js +12 -12
- package/dist/collab/first-work.d.ts +3 -0
- package/dist/collab/first-work.js +15 -0
- package/dist/collab/handoff.d.ts +16 -0
- package/dist/collab/handoff.js +39 -5
- package/dist/collab/index.d.ts +97 -1
- package/dist/collab/index.js +9 -1
- package/dist/collab/messages.js +126 -3
- package/dist/collab/persisted-messages.d.ts +123 -0
- package/dist/collab/persisted-messages.js +496 -0
- package/dist/collab/preflight.d.ts +1 -1
- package/dist/collab/resources.d.ts +13 -3
- package/dist/collab/resources.js +37 -6
- package/dist/collab/rollover.d.ts +311 -0
- package/dist/collab/rollover.js +1024 -0
- package/dist/collab/store.d.ts +1 -0
- package/dist/collab/store.js +1 -4
- package/dist/collab/types.d.ts +2 -354
- package/dist/collab/validation.d.ts +2 -10
- package/dist/collab/validation.js +2 -303
- package/dist/collab/work-plan.d.ts +105 -0
- package/dist/collab/work-plan.js +843 -0
- package/dist/continuity/context.d.ts +11 -2
- package/dist/continuity/context.js +100 -13
- package/dist/continuity/handoff-bundle.d.ts +296 -0
- package/dist/continuity/handoff-bundle.js +477 -0
- package/dist/continuity/index.d.ts +2 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.js +10 -2
- package/dist/continuity/service.js +91 -15
- package/dist/continuity/task-pin.d.ts +24 -0
- package/dist/continuity/task-pin.js +590 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +6 -0
- package/dist/experimental/index.d.ts +13 -0
- package/dist/experimental/index.js +25 -0
- package/dist/experimental/pi.d.ts +4 -0
- package/dist/experimental/pi.js +12 -0
- package/dist/hosts/claude/native.js +1 -1
- package/dist/hosts/codex/native.js +1 -1
- package/dist/hosts/codex/protocol.js +1 -1
- package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
- package/dist/hosts/integration/bootstrap-config.js +254 -0
- package/dist/hosts/integration/bundle-export.d.ts +44 -0
- package/dist/hosts/integration/bundle-export.js +366 -3
- package/dist/hosts/integration/bundle.js +3 -0
- package/dist/hosts/integration/hook.js +36 -9
- package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
- package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
- package/dist/hosts/integration/pappra-hook.mjs +61 -24749
- package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
- package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
- package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
- package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
- package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
- package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
- package/dist/hosts/integration/pi-native-events.d.ts +112 -0
- package/dist/hosts/integration/pi-native-events.js +348 -0
- package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
- package/dist/hosts/integration/pi-rollover-extension.js +431 -0
- package/dist/hosts/integration/recovery-config.d.ts +48 -0
- package/dist/hosts/integration/recovery-config.js +117 -0
- package/dist/hosts/integration/service.d.ts +6 -1
- package/dist/hosts/integration/service.js +485 -35
- package/dist/hosts/integration/support.d.ts +7 -0
- package/dist/hosts/integration/support.js +9 -1
- package/dist/hosts/integration/templates.d.ts +3 -0
- package/dist/hosts/integration/templates.js +26 -3
- package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
- package/dist/hosts/pi/managed-host-identity.js +40 -0
- package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
- package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
- package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
- package/dist/hosts/pi/managed-sdk-session.js +1639 -0
- package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
- package/dist/hosts/pi/managed-session-extension.js +84 -0
- package/dist/hosts/pi/managed-session.d.ts +177 -0
- package/dist/hosts/pi/managed-session.js +1557 -0
- package/dist/hosts/pi/protocol.js +1 -1
- package/dist/hosts/pi/query-result.js +1 -1
- package/dist/hosts/pi/resume-capability.d.ts +41 -0
- package/dist/hosts/pi/resume-capability.js +33 -0
- package/dist/hosts/pi/rpc.d.ts +182 -134
- package/dist/hosts/pi/rpc.js +230 -37
- package/dist/hosts/pilot.d.ts +97 -74
- package/dist/hosts/pilot.js +5 -1
- package/dist/hosts/version.d.ts +3 -0
- package/dist/hosts/version.js +18 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/bounded-file.d.ts +19 -0
- package/dist/infra/bounded-file.js +66 -0
- package/dist/infra/fs.js +10 -2
- package/dist/infra/git.d.ts +29 -0
- package/dist/infra/git.js +45 -9
- package/dist/infra/json-property.d.ts +2 -0
- package/dist/infra/json-property.js +69 -0
- package/dist/infra/json.d.ts +4 -0
- package/dist/infra/json.js +24 -0
- package/dist/infra/owned-process-group.js +1 -1
- package/dist/infra/private-lock.d.ts +16 -0
- package/dist/infra/private-lock.js +148 -0
- package/dist/infra/private-state.d.ts +7 -0
- package/dist/infra/private-state.js +146 -0
- package/dist/infra/process-identity.d.ts +12 -0
- package/dist/infra/process-identity.js +142 -0
- package/dist/infra/validation.d.ts +5 -0
- package/dist/infra/validation.js +15 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +6 -0
- package/dist/install/apply.d.ts +9 -1
- package/dist/install/apply.js +176 -12
- package/dist/install/dependencies.d.ts +38 -0
- package/dist/install/dependencies.js +282 -0
- package/dist/install/ownership.js +30 -1
- package/dist/install/references.d.ts +31 -0
- package/dist/install/references.js +208 -0
- package/dist/install/repair.d.ts +37 -0
- package/dist/install/repair.js +798 -0
- package/dist/install/reset-local.d.ts +16 -0
- package/dist/install/reset-local.js +304 -0
- package/dist/install/rollback.d.ts +3 -0
- package/dist/install/rollback.js +9 -3
- package/dist/install/runtime.d.ts +10 -6
- package/dist/install/runtime.js +299 -59
- package/dist/install/source-fingerprint.d.ts +2 -0
- package/dist/install/source-fingerprint.js +103 -0
- package/dist/knowledge/catalog.d.ts +1 -0
- package/dist/knowledge/catalog.js +143 -10
- package/dist/knowledge/journal.js +17 -7
- package/dist/knowledge/paths.js +2 -1
- package/dist/knowledge/refs.js +4 -0
- package/dist/knowledge/register-batch.d.ts +4 -0
- package/dist/knowledge/register-batch.js +225 -0
- package/dist/knowledge/relocate.d.ts +5 -0
- package/dist/knowledge/relocate.js +92 -0
- package/dist/knowledge/scan.js +103 -47
- package/dist/knowledge/service.d.ts +3 -0
- package/dist/knowledge/service.js +33 -1
- package/dist/knowledge/write.d.ts +4 -1
- package/dist/knowledge/write.js +1 -1
- package/dist/mcp/server.d.ts +4 -2
- package/dist/mcp/server.js +87 -136
- package/dist/schemas/collaboration-state-validation.d.ts +6 -0
- package/dist/schemas/collaboration-state-validation.js +342 -0
- package/dist/schemas/collaboration-state.d.ts +436 -0
- package/dist/schemas/collaboration-state.js +1 -0
- package/dist/schemas/collaboration.d.ts +335 -0
- package/dist/schemas/collaboration.js +272 -0
- package/dist/schemas/continuity.d.ts +8 -0
- package/dist/schemas/continuity.js +13 -3
- package/dist/schemas/doctor.d.ts +1 -0
- package/dist/schemas/doctor.js +1 -0
- package/dist/schemas/document.d.ts +61 -0
- package/dist/schemas/document.js +9 -0
- package/dist/schemas/errors.d.ts +1 -0
- package/dist/schemas/errors.js +1 -0
- package/dist/schemas/host.d.ts +24 -0
- package/dist/schemas/host.js +19 -0
- package/dist/schemas/install.d.ts +8 -2
- package/dist/schemas/install.js +11 -1
- package/dist/schemas/managed-pi-execution.d.ts +16 -0
- package/dist/schemas/managed-pi-execution.js +2 -0
- package/dist/schemas/outcome.d.ts +9 -5
- package/dist/workspace/config-file.js +1 -3
- package/dist/workspace/documents.js +12 -2
- package/dist/workspace/repositories.d.ts +7 -0
- package/dist/workspace/repositories.js +14 -5
- package/package.json +35 -35
- package/skills/pappra/SKILL.md +28 -7
- package/skills/pappra/references/collaboration.md +633 -0
- package/skills/pappra/references/environment.md +360 -0
- package/skills/pappra/references/hosts.md +20 -0
- package/skills/pappra/references/operations.md +36 -5
- package/dist/app/collab-from.d.ts +0 -35
- package/dist/app/collab-from.js +0 -84
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
import { object as isRecord } from '../infra/validation.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { contentDigest, isSafeMessageId, validateEnvelope, } from '../collab/envelope.js';
|
|
4
|
+
import { findHandoff } from '../collab/handoff.js';
|
|
5
|
+
import { findAttempt } from '../collab/lifecycle.js';
|
|
6
|
+
import * as rollover from '../collab/rollover.js';
|
|
7
|
+
import { isTaskRecord } from '../collab/validation.js';
|
|
8
|
+
import { handoffBundleRelativeRef, readHandoffBundle, verifyHandoffBundle, } from '../continuity/index.js';
|
|
9
|
+
import { resolveRealpath } from '../infra/fs.js';
|
|
10
|
+
import { isPathInside } from '../schemas/paths.js';
|
|
11
|
+
import { readBoundedFile } from './bounded-file.js';
|
|
12
|
+
import { routeExclusionFor, validateSuccession, } from './collab-route.js';
|
|
13
|
+
import { assertTaskStoreCurrent, openTaskStore } from './collab-task-store.js';
|
|
14
|
+
/**
|
|
15
|
+
* Public read-only rollover inspection and planning.
|
|
16
|
+
*
|
|
17
|
+
* The entry reads the exact authoritative task record and the one exact linked continuity
|
|
18
|
+
* handoff bundle, then reports the current rollover facts and the steps a later execution
|
|
19
|
+
* would need. It never mutates the task, claims, bundle, binding, notification or delivery
|
|
20
|
+
* files, and it exposes no apply path: a caller-supplied stop observation or producer label
|
|
21
|
+
* is not authenticated native proof, so host stop/freeze evidence, native launch, native
|
|
22
|
+
* session archive and routing removal are reported as unavailable.
|
|
23
|
+
*/
|
|
24
|
+
export const COLLAB_ROLLOVER_REQUEST_SCHEMA = 'pappra/collaboration-rollover-request/1';
|
|
25
|
+
export const COLLAB_ROLLOVER_REPORT_SCHEMA = 'pappra/collaboration-rollover-report/1';
|
|
26
|
+
const MAX_REQUEST_BYTES = 64 * 1024;
|
|
27
|
+
const MAX_STRING = 2048;
|
|
28
|
+
const MAX_ID = 256;
|
|
29
|
+
const MAX_EVIDENCE_FILES = 2;
|
|
30
|
+
const MAX_EVIDENCE_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
const MAX_EVIDENCE_FILE_BYTES = 1024 * 1024;
|
|
32
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
33
|
+
const failure = (kind, code, message, nextAction) => ({ ok: false, kind, code, message, nextAction });
|
|
34
|
+
const isText = (value, max = MAX_STRING) => typeof value === 'string' &&
|
|
35
|
+
value.length > 0 &&
|
|
36
|
+
value.length <= max &&
|
|
37
|
+
!value.includes('\u0000');
|
|
38
|
+
const isCount = (value, minimum = 0) => typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
|
|
39
|
+
const isIdentity = (value) => isRecord(value) &&
|
|
40
|
+
isText(value['agent_id'], MAX_ID) &&
|
|
41
|
+
isText(value['session_id'], MAX_ID);
|
|
42
|
+
const sameIdentity = (left, right) => left !== null &&
|
|
43
|
+
left.agent_id === right.agent_id &&
|
|
44
|
+
left.session_id === right.session_id;
|
|
45
|
+
const invalidRequest = (message) => failure('invalid', 'request_invalid', message, null);
|
|
46
|
+
function readIntent(value) {
|
|
47
|
+
if (!isRecord(value))
|
|
48
|
+
return invalidRequest('proposed_intent must be an object');
|
|
49
|
+
const intentId = value['intent_id'];
|
|
50
|
+
const generation = value['generation'];
|
|
51
|
+
const handoffId = value['handoff_id'];
|
|
52
|
+
const subtaskId = value['subtask_id'];
|
|
53
|
+
const attempt = value['attempt'];
|
|
54
|
+
const inputRevision = value['input_revision'];
|
|
55
|
+
const from = value['from'];
|
|
56
|
+
const to = value['to'];
|
|
57
|
+
const coordinatorEpoch = value['coordinator_epoch'];
|
|
58
|
+
if (!isText(intentId, MAX_ID) ||
|
|
59
|
+
!isCount(generation, 1) ||
|
|
60
|
+
!isText(handoffId, MAX_ID) ||
|
|
61
|
+
!isText(subtaskId, MAX_ID) ||
|
|
62
|
+
!isCount(attempt, 1) ||
|
|
63
|
+
!isText(inputRevision) ||
|
|
64
|
+
!isIdentity(from) ||
|
|
65
|
+
!isIdentity(to) ||
|
|
66
|
+
!isCount(coordinatorEpoch, 1))
|
|
67
|
+
return invalidRequest('proposed_intent needs intent, handoff, subtask, attempt, input revision, identities and a positive coordinator epoch');
|
|
68
|
+
const claimsRaw = value['claims'];
|
|
69
|
+
if (!Array.isArray(claimsRaw))
|
|
70
|
+
return invalidRequest('proposed_intent.claims must be an array');
|
|
71
|
+
if (claimsRaw.length > 64)
|
|
72
|
+
return invalidRequest('proposed_intent.claims exceeds the claim limit');
|
|
73
|
+
const claims = [];
|
|
74
|
+
for (const claim of claimsRaw) {
|
|
75
|
+
if (!isRecord(claim))
|
|
76
|
+
return invalidRequest('each claim reference must be an object');
|
|
77
|
+
const claimId = claim['claim_id'];
|
|
78
|
+
const resourceEpoch = claim['resource_epoch'];
|
|
79
|
+
if (!isText(claimId, MAX_ID) || !isCount(resourceEpoch, 1))
|
|
80
|
+
return invalidRequest('each claim reference needs an id and a positive epoch');
|
|
81
|
+
claims.push({ claim_id: claimId, resource_epoch: resourceEpoch });
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
value: {
|
|
86
|
+
intent_id: intentId,
|
|
87
|
+
generation,
|
|
88
|
+
handoff_id: handoffId,
|
|
89
|
+
subtask_id: subtaskId,
|
|
90
|
+
attempt,
|
|
91
|
+
input_revision: inputRevision,
|
|
92
|
+
from,
|
|
93
|
+
to,
|
|
94
|
+
claims,
|
|
95
|
+
coordinator_epoch: coordinatorEpoch,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Validates one request value. The CLI loader and the public app entry both call this, so a
|
|
101
|
+
* direct caller cannot skip the documented shape by constructing an object in memory.
|
|
102
|
+
*/
|
|
103
|
+
export function validateRolloverRequest(parsed) {
|
|
104
|
+
if (!isRecord(parsed))
|
|
105
|
+
return invalidRequest('request must be an object');
|
|
106
|
+
if (parsed['schema'] !== COLLAB_ROLLOVER_REQUEST_SCHEMA)
|
|
107
|
+
return failure('invalid', 'request_schema_invalid', `request.schema must be ${COLLAB_ROLLOVER_REQUEST_SCHEMA}`, 'Use the documented request schema.');
|
|
108
|
+
const action = parsed['action'];
|
|
109
|
+
if (action !== 'inspect' && action !== 'plan')
|
|
110
|
+
return failure('invalid', 'request_action_invalid', 'action must be inspect or plan', 'This entry never applies a rollover.');
|
|
111
|
+
const taskRoot = parsed['task_root'];
|
|
112
|
+
if (!isText(taskRoot))
|
|
113
|
+
return invalidRequest('task_root is required');
|
|
114
|
+
if (!path.isAbsolute(taskRoot))
|
|
115
|
+
return failure('invalid', 'request_invalid', 'task_root must be absolute', 'Pass the absolute task root that contains task.json.');
|
|
116
|
+
const bundleRaw = parsed['bundle'];
|
|
117
|
+
if (!isRecord(bundleRaw))
|
|
118
|
+
return invalidRequest('bundle is required');
|
|
119
|
+
const bundleRoot = bundleRaw['root'];
|
|
120
|
+
const continuityTaskId = bundleRaw['continuity_task_id'];
|
|
121
|
+
const bundleId = bundleRaw['bundle_id'];
|
|
122
|
+
const bundleDigest = bundleRaw['digest'];
|
|
123
|
+
if (!isText(bundleRoot) ||
|
|
124
|
+
!path.isAbsolute(bundleRoot) ||
|
|
125
|
+
!isText(continuityTaskId, MAX_ID) ||
|
|
126
|
+
!isText(bundleId, MAX_ID) ||
|
|
127
|
+
typeof bundleDigest !== 'string' ||
|
|
128
|
+
!DIGEST.test(bundleDigest))
|
|
129
|
+
return invalidRequest('bundle needs an absolute root, continuity_task_id, bundle_id and sha256 digest');
|
|
130
|
+
const intentIdRaw = parsed['intent_id'];
|
|
131
|
+
if (intentIdRaw !== undefined &&
|
|
132
|
+
intentIdRaw !== null &&
|
|
133
|
+
!isText(intentIdRaw, MAX_ID))
|
|
134
|
+
return invalidRequest('intent_id is invalid');
|
|
135
|
+
const intentId = isText(intentIdRaw, MAX_ID) ? intentIdRaw : null;
|
|
136
|
+
const proposedRaw = parsed['proposed_intent'];
|
|
137
|
+
let proposed = null;
|
|
138
|
+
if (proposedRaw !== undefined && proposedRaw !== null) {
|
|
139
|
+
const readProposed = readIntent(proposedRaw);
|
|
140
|
+
if (readProposed.ok === false)
|
|
141
|
+
return readProposed;
|
|
142
|
+
proposed = readProposed.value;
|
|
143
|
+
}
|
|
144
|
+
if (action === 'inspect' && intentId === null)
|
|
145
|
+
return failure('invalid', 'request_invalid', 'inspect requires intent_id', 'Pass the recorded intent id to inspect.');
|
|
146
|
+
if (action === 'plan' && proposed === null && intentId === null)
|
|
147
|
+
return failure('invalid', 'request_invalid', 'plan requires intent_id or proposed_intent', 'Describe the intended rollover.');
|
|
148
|
+
const evidenceRaw = parsed['evidence'];
|
|
149
|
+
if (evidenceRaw !== undefined &&
|
|
150
|
+
evidenceRaw !== null &&
|
|
151
|
+
!isRecord(evidenceRaw))
|
|
152
|
+
return invalidRequest('evidence must be an object');
|
|
153
|
+
const ackRaw = isRecord(evidenceRaw) ? evidenceRaw['ack_message_id'] : null;
|
|
154
|
+
const reportRaw = isRecord(evidenceRaw)
|
|
155
|
+
? evidenceRaw['report_message_id']
|
|
156
|
+
: null;
|
|
157
|
+
for (const [key, value] of [
|
|
158
|
+
['ack_message_id', ackRaw],
|
|
159
|
+
['report_message_id', reportRaw],
|
|
160
|
+
]) {
|
|
161
|
+
if (value !== undefined && value !== null && !isText(value, MAX_ID))
|
|
162
|
+
return invalidRequest(`${key} is invalid`);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
value: {
|
|
167
|
+
schema: COLLAB_ROLLOVER_REQUEST_SCHEMA,
|
|
168
|
+
action,
|
|
169
|
+
task_root: taskRoot,
|
|
170
|
+
intent_id: intentId,
|
|
171
|
+
proposed_intent: proposed,
|
|
172
|
+
bundle: {
|
|
173
|
+
root: bundleRoot,
|
|
174
|
+
continuity_task_id: continuityTaskId,
|
|
175
|
+
bundle_id: bundleId,
|
|
176
|
+
digest: bundleDigest,
|
|
177
|
+
},
|
|
178
|
+
evidence: {
|
|
179
|
+
ack_message_id: isText(ackRaw, MAX_ID) ? ackRaw : null,
|
|
180
|
+
report_message_id: isText(reportRaw, MAX_ID) ? reportRaw : null,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Loads one bounded request document; unsafe or oversized input is invalid, never read. */
|
|
186
|
+
export function loadRolloverRequest(file) {
|
|
187
|
+
const read = readBoundedFile(file, MAX_REQUEST_BYTES);
|
|
188
|
+
if (!read.ok)
|
|
189
|
+
return failure('invalid', 'request_unreadable', `The rollover request could not be read as a bounded regular file (${read.code}).`, 'Pass an absolute path to a regular JSON request file.');
|
|
190
|
+
let parsed;
|
|
191
|
+
try {
|
|
192
|
+
parsed = JSON.parse(read.text);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return failure('invalid', 'request_invalid_json', 'The rollover request is not valid JSON.', 'Fix the request document syntax.');
|
|
196
|
+
}
|
|
197
|
+
return validateRolloverRequest(parsed);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Bounded evidence loading for the requested ids only.
|
|
201
|
+
*
|
|
202
|
+
* There is no helper pre-read: the id is validated, the path is contained in the opened
|
|
203
|
+
* task namespace, and the file's parent is re-proved by realpath before `readBoundedFile`
|
|
204
|
+
* opens it descriptor-safely (nonblocking, no-follow, regular-file, size-capped). Actual raw
|
|
205
|
+
* bytes are charged immediately after a successful read, before JSON or envelope refusal, so
|
|
206
|
+
* a corrupt or padded file can never be inspected for free.
|
|
207
|
+
*/
|
|
208
|
+
async function loadEvidence(task, ids) {
|
|
209
|
+
const messages = [];
|
|
210
|
+
const entries = new Map();
|
|
211
|
+
const indices = new Map();
|
|
212
|
+
const unreadable = [];
|
|
213
|
+
const ledger = task.record.messages ?? [];
|
|
214
|
+
const byId = new Map(ledger.map((entry, index) => [entry.message_id, index]));
|
|
215
|
+
let rawBytes = 0;
|
|
216
|
+
let overBudget = false;
|
|
217
|
+
for (const messageId of ids) {
|
|
218
|
+
if (!isSafeMessageId(messageId)) {
|
|
219
|
+
unreadable.push({ message_id: messageId, code: 'message_id_invalid' });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const file = path.join(task.messagesDirectory, `${messageId}.json`);
|
|
223
|
+
if (!isPathInside(task.messagesDirectory, file)) {
|
|
224
|
+
unreadable.push({ message_id: messageId, code: 'message_id_invalid' });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const index = byId.get(messageId);
|
|
228
|
+
if (index === undefined) {
|
|
229
|
+
unreadable.push({
|
|
230
|
+
message_id: messageId,
|
|
231
|
+
code: 'message_not_registered',
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const resolvedDirectory = await resolveRealpath(path.dirname(file)).catch(() => null);
|
|
236
|
+
if (resolvedDirectory === null ||
|
|
237
|
+
resolvedDirectory !== task.messagesDirectory) {
|
|
238
|
+
unreadable.push({
|
|
239
|
+
message_id: messageId,
|
|
240
|
+
code: 'message_registration_invalid',
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const remaining = Math.min(MAX_EVIDENCE_FILE_BYTES, MAX_EVIDENCE_BYTES - rawBytes);
|
|
245
|
+
if (remaining <= 0) {
|
|
246
|
+
overBudget = true;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
const read = readBoundedFile(file, remaining);
|
|
250
|
+
if (read.ok === false) {
|
|
251
|
+
if (read.code === 'too_large') {
|
|
252
|
+
overBudget = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
unreadable.push({ message_id: messageId, code: read.code });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// The bytes are consumed before any later refusal, including rejected content.
|
|
259
|
+
rawBytes += read.bytes.length;
|
|
260
|
+
let payload;
|
|
261
|
+
try {
|
|
262
|
+
payload = JSON.parse(read.text);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
unreadable.push({ message_id: messageId, code: 'message_corrupt' });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
let message;
|
|
269
|
+
try {
|
|
270
|
+
message = validateEnvelope(payload);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
unreadable.push({ message_id: messageId, code: 'message_invalid' });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (message.message_id !== messageId) {
|
|
277
|
+
unreadable.push({
|
|
278
|
+
message_id: messageId,
|
|
279
|
+
code: 'message_identity_mismatch',
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const entry = ledger[index];
|
|
284
|
+
if (entry.state !== 'applied') {
|
|
285
|
+
unreadable.push({ message_id: messageId, code: 'message_not_applied' });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (entry.content_digest !== contentDigest(message)) {
|
|
289
|
+
unreadable.push({ message_id: messageId, code: 'content_mismatch' });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
messages.push(message);
|
|
293
|
+
entries.set(messageId, entry);
|
|
294
|
+
indices.set(messageId, index);
|
|
295
|
+
}
|
|
296
|
+
return { messages, entries, indices, rawBytes, unreadable, overBudget };
|
|
297
|
+
}
|
|
298
|
+
const planIntentOf = (input) => ({
|
|
299
|
+
intent_id: input.intent_id,
|
|
300
|
+
generation: input.generation,
|
|
301
|
+
handoff_id: input.handoff_id,
|
|
302
|
+
subtask_id: input.subtask_id,
|
|
303
|
+
attempt: input.attempt,
|
|
304
|
+
input_revision: input.input_revision,
|
|
305
|
+
from: input.from,
|
|
306
|
+
to: input.to,
|
|
307
|
+
claims: input.claims,
|
|
308
|
+
coordinator_epoch: input.coordinator_epoch,
|
|
309
|
+
});
|
|
310
|
+
const intentOf = (intent) => ({
|
|
311
|
+
intent_id: intent.intent_id,
|
|
312
|
+
generation: intent.generation,
|
|
313
|
+
handoff_id: intent.handoff_id,
|
|
314
|
+
subtask_id: intent.subtask_id,
|
|
315
|
+
attempt: intent.attempt,
|
|
316
|
+
input_revision: intent.input_revision,
|
|
317
|
+
from: intent.from,
|
|
318
|
+
to: intent.to,
|
|
319
|
+
claims: intent.claims,
|
|
320
|
+
coordinator_epoch: intent.coordinator_epoch,
|
|
321
|
+
});
|
|
322
|
+
const STAGE_BY_HANDOFF = {
|
|
323
|
+
requested: 'pre-transfer',
|
|
324
|
+
old_writer_stopped: 'old-writer-stopped',
|
|
325
|
+
transferring: 'transferring',
|
|
326
|
+
receiver_confirmed: 'receiver-confirmed',
|
|
327
|
+
completed: 'completed',
|
|
328
|
+
aborted: 'aborted',
|
|
329
|
+
};
|
|
330
|
+
const STAGE_OF = (state) => state === null ? 'unknown' : (STAGE_BY_HANDOFF[state] ?? 'unknown');
|
|
331
|
+
const PREPARED_STAGES = [
|
|
332
|
+
'old-writer-stopped',
|
|
333
|
+
'transferring',
|
|
334
|
+
'receiver-confirmed',
|
|
335
|
+
'completed',
|
|
336
|
+
];
|
|
337
|
+
const CONFIRMED_STAGES = [
|
|
338
|
+
'receiver-confirmed',
|
|
339
|
+
'completed',
|
|
340
|
+
];
|
|
341
|
+
const isComplete = (value) => value === 'complete';
|
|
342
|
+
/**
|
|
343
|
+
* Compares the verified bundle against the record and the plan. Bundle context is evidence
|
|
344
|
+
* about a workspace snapshot; it never grants authority to apply anything.
|
|
345
|
+
*/
|
|
346
|
+
function bundleAssociation(options) {
|
|
347
|
+
const collaboration = options.bundle.collaboration;
|
|
348
|
+
const sourceSession = options.bundle.sourceNativeSession;
|
|
349
|
+
const sourceAgent = options.pinnedSource?.agent_id ?? options.plan.from.agent_id;
|
|
350
|
+
const sourceSessionId = options.pinnedSource?.session_id ?? options.plan.from.session_id;
|
|
351
|
+
const sourceHarness = options.plan.from.harness;
|
|
352
|
+
const sessionMatches = sourceSession.sessionId === sourceSessionId &&
|
|
353
|
+
sourceSession.agentId === sourceAgent &&
|
|
354
|
+
(sourceHarness === undefined || sourceSession.harness === sourceHarness);
|
|
355
|
+
const collaborationMatches = collaboration.taskId === options.record.task_id &&
|
|
356
|
+
collaboration.subtaskId === options.plan.subtask_id &&
|
|
357
|
+
collaboration.attempt === options.plan.attempt &&
|
|
358
|
+
collaboration.ownerEpoch === options.plan.coordinator_epoch &&
|
|
359
|
+
collaboration.inputRevision === options.plan.input_revision;
|
|
360
|
+
const snapshot = options.bundle.handoff;
|
|
361
|
+
const claimMismatches = [];
|
|
362
|
+
for (const reference of options.plan.claims) {
|
|
363
|
+
const snapshotClaim = snapshot.claims.find((claim) => claim.claimId === reference.claim_id);
|
|
364
|
+
if (snapshotClaim === undefined) {
|
|
365
|
+
if (isComplete(snapshot.claimsCompleteness))
|
|
366
|
+
claimMismatches.push(`claim ${reference.claim_id} missing from snapshot`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (snapshotClaim.resourceEpoch !== reference.resource_epoch)
|
|
370
|
+
claimMismatches.push(`claim ${reference.claim_id} epoch differs`);
|
|
371
|
+
if (snapshotClaim.holder !== options.plan.from.agent_id)
|
|
372
|
+
claimMismatches.push(`claim ${reference.claim_id} holder differs`);
|
|
373
|
+
}
|
|
374
|
+
const pending = [
|
|
375
|
+
{
|
|
376
|
+
kind: 'pending_tools',
|
|
377
|
+
items: snapshot.pendingTools.items.length,
|
|
378
|
+
completeness: snapshot.pendingTools.completeness,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
kind: 'pending_messages',
|
|
382
|
+
items: snapshot.pendingMessages.items.length,
|
|
383
|
+
completeness: snapshot.pendingMessages.completeness,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
kind: 'pending_approvals',
|
|
387
|
+
items: snapshot.pendingApprovals.items.length,
|
|
388
|
+
completeness: snapshot.pendingApprovals.completeness,
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
return {
|
|
392
|
+
source_session: {
|
|
393
|
+
harness: sourceSession.harness,
|
|
394
|
+
session_id: sourceSession.sessionId,
|
|
395
|
+
agent_id: sourceSession.agentId,
|
|
396
|
+
matches_plan: sessionMatches,
|
|
397
|
+
},
|
|
398
|
+
collaboration: {
|
|
399
|
+
task_id: collaboration.taskId,
|
|
400
|
+
subtask_id: collaboration.subtaskId,
|
|
401
|
+
attempt: collaboration.attempt,
|
|
402
|
+
owner_epoch: collaboration.ownerEpoch,
|
|
403
|
+
input_revision: collaboration.inputRevision,
|
|
404
|
+
association: collaboration.association,
|
|
405
|
+
matches_plan: collaborationMatches,
|
|
406
|
+
},
|
|
407
|
+
handoff_snapshot: {
|
|
408
|
+
claims: snapshot.claims.map((claim) => ({
|
|
409
|
+
claim_id: claim.claimId,
|
|
410
|
+
resource: claim.resource,
|
|
411
|
+
resource_epoch: claim.resourceEpoch,
|
|
412
|
+
holder: claim.holder,
|
|
413
|
+
})),
|
|
414
|
+
claims_completeness: snapshot.claimsCompleteness,
|
|
415
|
+
claims_match: claimMismatches.length === 0,
|
|
416
|
+
inventory_complete: isComplete(snapshot.claimsCompleteness) &&
|
|
417
|
+
pending.every((entry) => isComplete(entry.completeness) && entry.items === 0),
|
|
418
|
+
pending,
|
|
419
|
+
},
|
|
420
|
+
claimMismatches,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const STOP_PROOF_BLOCKER = {
|
|
424
|
+
code: 'host_stop_proof_unavailable',
|
|
425
|
+
detail: 'No accepted host adapter produces authenticated quiescence evidence; caller JSON is never accepted as stop proof.',
|
|
426
|
+
};
|
|
427
|
+
const predecessorBlocked = (detail) => ({
|
|
428
|
+
code: 'predecessor_step_unavailable',
|
|
429
|
+
detail,
|
|
430
|
+
});
|
|
431
|
+
/** Stable de-duplication: one blocker code is reported once, in first-seen order. */
|
|
432
|
+
const dedupeBlockers = (blockers) => {
|
|
433
|
+
const seen = new Set();
|
|
434
|
+
const unique = [];
|
|
435
|
+
for (const blocker of blockers) {
|
|
436
|
+
if (seen.has(blocker.code))
|
|
437
|
+
continue;
|
|
438
|
+
seen.add(blocker.code);
|
|
439
|
+
unique.push(blocker);
|
|
440
|
+
}
|
|
441
|
+
return unique;
|
|
442
|
+
};
|
|
443
|
+
function buildSteps(options) {
|
|
444
|
+
const aborted = options.stage === 'aborted';
|
|
445
|
+
const prepareSatisfied = options.prepared || PREPARED_STAGES.includes(options.stage);
|
|
446
|
+
const confirmSatisfied = CONFIRMED_STAGES.includes(options.stage);
|
|
447
|
+
const completeSatisfied = options.stage === 'completed';
|
|
448
|
+
const abortBlocker = aborted
|
|
449
|
+
? [
|
|
450
|
+
{
|
|
451
|
+
code: 'handoff_aborted',
|
|
452
|
+
detail: 'The recorded handoff is aborted; an aborted handoff never executes.',
|
|
453
|
+
},
|
|
454
|
+
]
|
|
455
|
+
: [];
|
|
456
|
+
return [
|
|
457
|
+
{
|
|
458
|
+
step: 'create-intent',
|
|
459
|
+
status: options.intentFound
|
|
460
|
+
? 'satisfied'
|
|
461
|
+
: options.createBlockers.length > 0
|
|
462
|
+
? 'blocked'
|
|
463
|
+
: 'ready',
|
|
464
|
+
requires: 'validated intent inputs, current coordinator epoch, latest active source attempt and source-held claims',
|
|
465
|
+
blockers: options.intentFound ? [] : options.createBlockers,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
step: 'prepare-transfer',
|
|
469
|
+
status: aborted
|
|
470
|
+
? 'blocked'
|
|
471
|
+
: prepareSatisfied
|
|
472
|
+
? 'satisfied'
|
|
473
|
+
: 'unavailable',
|
|
474
|
+
requires: 'authenticated host stop/freeze evidence plus a complete quiescence inventory',
|
|
475
|
+
blockers: aborted
|
|
476
|
+
? abortBlocker
|
|
477
|
+
: prepareSatisfied
|
|
478
|
+
? []
|
|
479
|
+
: [STOP_PROOF_BLOCKER],
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
step: 'confirm-receiver',
|
|
483
|
+
status: aborted
|
|
484
|
+
? 'blocked'
|
|
485
|
+
: confirmSatisfied
|
|
486
|
+
? 'satisfied'
|
|
487
|
+
: prepareSatisfied
|
|
488
|
+
? 'ready'
|
|
489
|
+
: 'blocked',
|
|
490
|
+
requires: 'registered successor ACK correlated to the handoff, generation, epoch and pinned input',
|
|
491
|
+
blockers: aborted
|
|
492
|
+
? abortBlocker
|
|
493
|
+
: confirmSatisfied || prepareSatisfied
|
|
494
|
+
? []
|
|
495
|
+
: [
|
|
496
|
+
predecessorBlocked('The receiver cannot be confirmed before transfer preparation, which requires host stop evidence.'),
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
step: 'complete-transfer',
|
|
501
|
+
status: aborted
|
|
502
|
+
? 'blocked'
|
|
503
|
+
: completeSatisfied
|
|
504
|
+
? 'satisfied'
|
|
505
|
+
: confirmSatisfied
|
|
506
|
+
? 'ready'
|
|
507
|
+
: 'blocked',
|
|
508
|
+
requires: 'receiver confirmation, exact claim epochs and an observed successor native identity',
|
|
509
|
+
blockers: aborted
|
|
510
|
+
? abortBlocker
|
|
511
|
+
: completeSatisfied || confirmSatisfied
|
|
512
|
+
? []
|
|
513
|
+
: [
|
|
514
|
+
predecessorBlocked('Ownership cannot transfer before the receiver confirms the handoff.'),
|
|
515
|
+
],
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
step: 'record-first-work',
|
|
519
|
+
status: aborted
|
|
520
|
+
? 'blocked'
|
|
521
|
+
: options.successionValid
|
|
522
|
+
? 'satisfied'
|
|
523
|
+
: completeSatisfied
|
|
524
|
+
? 'ready'
|
|
525
|
+
: 'blocked',
|
|
526
|
+
requires: 'validated succession: stored successor REPORT applied after the recorded transfer boundary and admitted as a completed result',
|
|
527
|
+
blockers: aborted
|
|
528
|
+
? abortBlocker
|
|
529
|
+
: options.successionValid
|
|
530
|
+
? []
|
|
531
|
+
: options.successionIssue !== null
|
|
532
|
+
? [
|
|
533
|
+
{
|
|
534
|
+
code: 'succession_invalid',
|
|
535
|
+
detail: options.successionIssue,
|
|
536
|
+
},
|
|
537
|
+
]
|
|
538
|
+
: completeSatisfied
|
|
539
|
+
? []
|
|
540
|
+
: [
|
|
541
|
+
predecessorBlocked('First successor work cannot be proven before ownership transfers.'),
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
function correlateEvidence(options) {
|
|
547
|
+
const { message, entry, index, plan, role } = options;
|
|
548
|
+
const issues = [];
|
|
549
|
+
const coordinator = options.record.coordinator ?? null;
|
|
550
|
+
const expectedType = role === 'ack' ? 'ACK' : 'REPORT';
|
|
551
|
+
const expectedIntentId = role === 'ack' ? plan.intent_id : null;
|
|
552
|
+
const inputRevision = typeof message.body.input_revision === 'string'
|
|
553
|
+
? message.body.input_revision
|
|
554
|
+
: null;
|
|
555
|
+
const intentId = typeof message.body.rollover_intent_id === 'string'
|
|
556
|
+
? message.body.rollover_intent_id
|
|
557
|
+
: null;
|
|
558
|
+
const generation = typeof message.body.rollover_generation === 'number'
|
|
559
|
+
? message.body.rollover_generation
|
|
560
|
+
: null;
|
|
561
|
+
if (message.task_id !== options.record.task_id)
|
|
562
|
+
issues.push('evidence_task_mismatch');
|
|
563
|
+
if (message.type !== expectedType)
|
|
564
|
+
issues.push('evidence_type_mismatch');
|
|
565
|
+
if (!sameIdentity(message.from, plan.to))
|
|
566
|
+
issues.push('evidence_sender_mismatch');
|
|
567
|
+
if (coordinator === null || !sameIdentity(message.to, coordinator))
|
|
568
|
+
issues.push('evidence_recipient_mismatch');
|
|
569
|
+
if (message.subtask_id !== plan.subtask_id ||
|
|
570
|
+
message.attempt !== plan.attempt)
|
|
571
|
+
issues.push('evidence_attempt_mismatch');
|
|
572
|
+
if (message.body.handoff_id !== plan.handoff_id)
|
|
573
|
+
issues.push('evidence_handoff_mismatch');
|
|
574
|
+
if (generation !== plan.generation)
|
|
575
|
+
issues.push('evidence_generation_mismatch');
|
|
576
|
+
if (inputRevision !== plan.input_revision)
|
|
577
|
+
issues.push('evidence_input_mismatch');
|
|
578
|
+
if (role === 'ack') {
|
|
579
|
+
if (intentId !== expectedIntentId)
|
|
580
|
+
issues.push('evidence_intent_mismatch');
|
|
581
|
+
if (message.owner_epoch !== plan.coordinator_epoch)
|
|
582
|
+
issues.push('evidence_epoch_mismatch');
|
|
583
|
+
}
|
|
584
|
+
else if (message.owner_epoch !== null &&
|
|
585
|
+
message.owner_epoch !== undefined) {
|
|
586
|
+
if (message.owner_epoch !== plan.coordinator_epoch)
|
|
587
|
+
issues.push('evidence_epoch_mismatch');
|
|
588
|
+
}
|
|
589
|
+
const proofOrderOk = role === 'report'
|
|
590
|
+
? options.boundary === null
|
|
591
|
+
? null
|
|
592
|
+
: index >= options.boundary.message_count
|
|
593
|
+
: null;
|
|
594
|
+
if (proofOrderOk === false)
|
|
595
|
+
issues.push('evidence_pre_transfer');
|
|
596
|
+
if (role === 'report') {
|
|
597
|
+
const result = (options.record.results ?? []).find((item) => item.message_id === message.message_id);
|
|
598
|
+
if (result === undefined)
|
|
599
|
+
issues.push('evidence_result_missing');
|
|
600
|
+
else if (result.status !== 'completed' ||
|
|
601
|
+
result.classification !== 'applied')
|
|
602
|
+
issues.push('evidence_result_not_admitted');
|
|
603
|
+
else if (result.subtask_id !== plan.subtask_id ||
|
|
604
|
+
result.attempt !== plan.attempt ||
|
|
605
|
+
result.input_revision !== plan.input_revision)
|
|
606
|
+
issues.push('evidence_result_mismatch');
|
|
607
|
+
else if (message.body.status !== result.status)
|
|
608
|
+
issues.push('evidence_result_status_mismatch');
|
|
609
|
+
}
|
|
610
|
+
if (entry.from.agent_id !== message.from.agent_id ||
|
|
611
|
+
entry.from.session_id !== message.from.session_id)
|
|
612
|
+
issues.push('evidence_ledger_sender_mismatch');
|
|
613
|
+
if (entry.to !== null &&
|
|
614
|
+
(entry.to.agent_id !== message.to.agent_id ||
|
|
615
|
+
entry.to.session_id !== message.to.session_id))
|
|
616
|
+
issues.push('evidence_ledger_recipient_mismatch');
|
|
617
|
+
if (entry.subtask_id !== message.subtask_id ||
|
|
618
|
+
entry.attempt !== message.attempt)
|
|
619
|
+
issues.push('evidence_ledger_attempt_mismatch');
|
|
620
|
+
return {
|
|
621
|
+
correlation: {
|
|
622
|
+
message_id: message.message_id,
|
|
623
|
+
role,
|
|
624
|
+
type: message.type,
|
|
625
|
+
digest: contentDigest(message),
|
|
626
|
+
sender: message.from,
|
|
627
|
+
recipient: message.to,
|
|
628
|
+
task_id: message.task_id,
|
|
629
|
+
subtask_id: message.subtask_id,
|
|
630
|
+
attempt: message.attempt,
|
|
631
|
+
input_revision: inputRevision,
|
|
632
|
+
intent_id: intentId,
|
|
633
|
+
generation,
|
|
634
|
+
owner_epoch: message.owner_epoch,
|
|
635
|
+
ledger_index: index,
|
|
636
|
+
proof_order_ok: proofOrderOk,
|
|
637
|
+
applies_to_intent: issues.length === 0,
|
|
638
|
+
issues,
|
|
639
|
+
},
|
|
640
|
+
issues: issues.map((code) => ({
|
|
641
|
+
code,
|
|
642
|
+
detail: `Requested ${role} evidence ${message.message_id} does not satisfy this correlation.`,
|
|
643
|
+
})),
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
export async function inspectOrPlanRollover(input) {
|
|
647
|
+
const validated = validateRolloverRequest(input);
|
|
648
|
+
if (validated.ok === false)
|
|
649
|
+
return validated;
|
|
650
|
+
const request = validated.value;
|
|
651
|
+
const opened = await openTaskStore(request.task_root);
|
|
652
|
+
if (opened.ok === false) {
|
|
653
|
+
const invalid = opened.code === 'task_root_required' ||
|
|
654
|
+
opened.code === 'task_root_layout_invalid';
|
|
655
|
+
return failure(invalid ? 'invalid' : 'needs_input', opened.code, opened.message, opened.nextAction);
|
|
656
|
+
}
|
|
657
|
+
const task = opened.value;
|
|
658
|
+
const record = task.record;
|
|
659
|
+
if (!isTaskRecord(record))
|
|
660
|
+
return failure('needs_input', 'record_invalid', 'The task record does not satisfy the authoritative record shape.', 'Inspect and repair the task record before planning.');
|
|
661
|
+
const recordedIntent = request.intent_id === null
|
|
662
|
+
? null
|
|
663
|
+
: ((record.rollover ?? []).find((item) => item.intent_id === request.intent_id) ?? null);
|
|
664
|
+
if (request.action === 'inspect' &&
|
|
665
|
+
request.intent_id !== null &&
|
|
666
|
+
recordedIntent === null)
|
|
667
|
+
return failure('needs_input', 'unknown_intent', `No recorded rollover intent ${request.intent_id} in this task.`, 'Inspect the record or pass an existing intent id.');
|
|
668
|
+
const proposed = request.proposed_intent;
|
|
669
|
+
const plan = recordedIntent !== null
|
|
670
|
+
? intentOf(recordedIntent)
|
|
671
|
+
: proposed !== null
|
|
672
|
+
? planIntentOf(proposed)
|
|
673
|
+
: null;
|
|
674
|
+
if (plan === null)
|
|
675
|
+
return failure('needs_input', 'unknown_intent', `No recorded rollover intent ${String(request.intent_id)} in this task.`, 'Pass an existing intent id or a proposed intent.');
|
|
676
|
+
// The canonical bundle reference convention is one documented form: the continuity task
|
|
677
|
+
// namespace plus `handoff-bundles/<bundleId>.json`. A recorded intent's pin is immutable.
|
|
678
|
+
const canonicalRef = handoffBundleRelativeRef(request.bundle.bundle_id);
|
|
679
|
+
if (recordedIntent !== null) {
|
|
680
|
+
const pinned = recordedIntent.bundle;
|
|
681
|
+
if (pinned.digest !== request.bundle.digest || pinned.ref !== canonicalRef)
|
|
682
|
+
return failure('needs_input', 'bundle_pin_mismatch', `The recorded intent pins ${pinned.ref} (${pinned.digest}); the request names ${canonicalRef} (${request.bundle.digest}).`, 'Reference the exact bundle recorded at intent creation; a different valid bundle is not a substitute.');
|
|
683
|
+
}
|
|
684
|
+
const bundleRead = await readHandoffBundle({
|
|
685
|
+
root: request.bundle.root,
|
|
686
|
+
continuityTaskId: request.bundle.continuity_task_id,
|
|
687
|
+
bundleId: request.bundle.bundle_id,
|
|
688
|
+
});
|
|
689
|
+
if (bundleRead.ok === false)
|
|
690
|
+
return failure('needs_input', 'bundle_unreadable', bundleRead.message, bundleRead.nextAction);
|
|
691
|
+
if (bundleRead.value.digest !== request.bundle.digest)
|
|
692
|
+
return failure('needs_input', 'bundle_digest_mismatch', `The stored bundle digest is ${bundleRead.value.digest}; the request pins ${request.bundle.digest}.`, 'Re-pin the exact linked bundle digest.');
|
|
693
|
+
const verified = await verifyHandoffBundle({
|
|
694
|
+
root: request.bundle.root,
|
|
695
|
+
bundle: bundleRead.value.bundle,
|
|
696
|
+
});
|
|
697
|
+
if (verified.ok === false)
|
|
698
|
+
return failure('needs_input', 'bundle_unverified', verified.message, verified.nextAction);
|
|
699
|
+
const handoff = findHandoff(record, plan.handoff_id);
|
|
700
|
+
const attempt = findAttempt(record, plan.subtask_id, plan.attempt);
|
|
701
|
+
const currentEpoch = record.coordinator?.owner_epoch ?? null;
|
|
702
|
+
const pinnedSource = recordedIntent?.source_native ?? null;
|
|
703
|
+
const stage = STAGE_OF(handoff?.state ?? null);
|
|
704
|
+
const postTransfer = stage === 'completed';
|
|
705
|
+
const successorNative = recordedIntent?.successor_native ?? null;
|
|
706
|
+
const boundary = recordedIntent?.transfer_boundary ?? null;
|
|
707
|
+
const drift = [];
|
|
708
|
+
if (currentEpoch !== plan.coordinator_epoch)
|
|
709
|
+
drift.push({
|
|
710
|
+
code: 'stale_epoch',
|
|
711
|
+
detail: `The plan pins coordinator epoch ${plan.coordinator_epoch}; the current epoch is ${String(currentEpoch)}.`,
|
|
712
|
+
});
|
|
713
|
+
if (handoff === null)
|
|
714
|
+
drift.push({
|
|
715
|
+
code: 'unknown_handoff',
|
|
716
|
+
detail: 'The planned handoff is not recorded in this task.',
|
|
717
|
+
});
|
|
718
|
+
if (postTransfer && boundary === null)
|
|
719
|
+
drift.push({
|
|
720
|
+
code: 'transfer_boundary_missing',
|
|
721
|
+
detail: 'The handoff is completed but the intent records no transfer boundary; post-transfer facts cannot be correlated.',
|
|
722
|
+
});
|
|
723
|
+
if (attempt === null)
|
|
724
|
+
drift.push({
|
|
725
|
+
code: 'unknown_attempt',
|
|
726
|
+
detail: 'The planned attempt is not recorded in this task.',
|
|
727
|
+
});
|
|
728
|
+
else {
|
|
729
|
+
const latest = (record.attempts ?? [])
|
|
730
|
+
.filter((item) => item.subtask_id === plan.subtask_id)
|
|
731
|
+
.reduce((max, item) => Math.max(max, item.attempt), 0);
|
|
732
|
+
if (latest !== plan.attempt && !postTransfer)
|
|
733
|
+
drift.push({
|
|
734
|
+
code: 'attempt_superseded',
|
|
735
|
+
detail: `The latest attempt is ${latest}; the plan pins ${plan.attempt}.`,
|
|
736
|
+
});
|
|
737
|
+
const expectedAssignee = postTransfer ? plan.to : plan.from;
|
|
738
|
+
if (!sameIdentity(attempt.assignee, expectedAssignee))
|
|
739
|
+
drift.push({
|
|
740
|
+
code: 'assignee_mismatch',
|
|
741
|
+
detail: postTransfer
|
|
742
|
+
? 'The completed transfer did not reassign the attempt to the successor identity.'
|
|
743
|
+
: 'The recorded assignee is not the planned source identity.',
|
|
744
|
+
});
|
|
745
|
+
if (attempt.input_revision !== plan.input_revision)
|
|
746
|
+
drift.push({
|
|
747
|
+
code: 'input_changed',
|
|
748
|
+
detail: `The attempt is pinned to ${String(attempt.input_revision)}; the plan to ${plan.input_revision}.`,
|
|
749
|
+
});
|
|
750
|
+
if (attempt.awaiting !== undefined && !postTransfer)
|
|
751
|
+
drift.push({
|
|
752
|
+
code: 'writer_blocked',
|
|
753
|
+
detail: `The recorded attempt is waiting for ${attempt.awaiting}.`,
|
|
754
|
+
});
|
|
755
|
+
for (const reference of plan.claims) {
|
|
756
|
+
const claim = (record.claims ?? []).find((item) => item.claim_id === reference.claim_id);
|
|
757
|
+
const expectedEpoch = postTransfer
|
|
758
|
+
? reference.resource_epoch + 1
|
|
759
|
+
: reference.resource_epoch;
|
|
760
|
+
if (claim === undefined || claim.released_at !== null) {
|
|
761
|
+
drift.push({
|
|
762
|
+
code: 'claim_unavailable',
|
|
763
|
+
detail: `Claim ${reference.claim_id} is missing or already released.`,
|
|
764
|
+
});
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (claim.holder === null ||
|
|
768
|
+
!sameIdentity(claim.holder, expectedAssignee)) {
|
|
769
|
+
drift.push({
|
|
770
|
+
code: 'claim_holder_mismatch',
|
|
771
|
+
detail: postTransfer
|
|
772
|
+
? `Claim ${reference.claim_id} is not held by the successor after the completed transfer.`
|
|
773
|
+
: `Claim ${reference.claim_id} is not held by the planned source.`,
|
|
774
|
+
});
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if ((claim.resource_epoch ?? 0) !== expectedEpoch)
|
|
778
|
+
drift.push({
|
|
779
|
+
code: 'claim_epoch_mismatch',
|
|
780
|
+
detail: `Claim ${reference.claim_id} is at epoch ${String(claim.resource_epoch)}; the ${postTransfer ? 'post-transfer' : 'pre-transfer'} expectation is ${expectedEpoch}.`,
|
|
781
|
+
});
|
|
782
|
+
if (postTransfer &&
|
|
783
|
+
claim.transferred_from !== undefined &&
|
|
784
|
+
!sameIdentity(claim.transferred_from, plan.from))
|
|
785
|
+
drift.push({
|
|
786
|
+
code: 'claim_transfer_source_mismatch',
|
|
787
|
+
detail: `Claim ${reference.claim_id} was not transferred from the planned source identity.`,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
const native = attempt.native;
|
|
791
|
+
const expectedNative = postTransfer ? successorNative : pinnedSource;
|
|
792
|
+
if (native.observation !== 'confirmed')
|
|
793
|
+
drift.push({
|
|
794
|
+
code: 'native_unconfirmed',
|
|
795
|
+
detail: `The recorded attempt native observation is ${native.observation}.`,
|
|
796
|
+
});
|
|
797
|
+
else if (expectedNative !== null) {
|
|
798
|
+
if (expectedNative.session_id !== native.session_id ||
|
|
799
|
+
expectedNative.runner !== native.runner ||
|
|
800
|
+
expectedNative.run_id !== native.run_id)
|
|
801
|
+
drift.push({
|
|
802
|
+
code: postTransfer ? 'successor_native_drift' : 'source_native_drift',
|
|
803
|
+
detail: postTransfer
|
|
804
|
+
? 'The recorded native identity no longer matches the pinned successor.'
|
|
805
|
+
: 'The recorded source native identity no longer matches the identity pinned at intent creation.',
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// The accepted reducer is the authority on refusals; it runs on a clone and never persists.
|
|
810
|
+
const createBlockers = [];
|
|
811
|
+
if (recordedIntent === null && proposed !== null) {
|
|
812
|
+
const simulated = rollover.createRolloverIntent(structuredClone(record), {
|
|
813
|
+
intent_id: plan.intent_id,
|
|
814
|
+
generation: plan.generation,
|
|
815
|
+
handoff_id: plan.handoff_id,
|
|
816
|
+
subtask_id: plan.subtask_id,
|
|
817
|
+
attempt: plan.attempt,
|
|
818
|
+
input_revision: plan.input_revision,
|
|
819
|
+
from: plan.from,
|
|
820
|
+
to: plan.to,
|
|
821
|
+
claims: [...plan.claims],
|
|
822
|
+
bundle: { ref: canonicalRef, digest: request.bundle.digest },
|
|
823
|
+
coordinator_epoch: plan.coordinator_epoch,
|
|
824
|
+
});
|
|
825
|
+
if (simulated.ok === false)
|
|
826
|
+
createBlockers.push({
|
|
827
|
+
code: simulated.reason,
|
|
828
|
+
detail: `The accepted reducer refuses the planned intent (${simulated.reason}).`,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const association = bundleAssociation({
|
|
832
|
+
bundle: bundleRead.value.bundle,
|
|
833
|
+
record,
|
|
834
|
+
plan,
|
|
835
|
+
pinnedSource,
|
|
836
|
+
});
|
|
837
|
+
if (!association.collaboration.matches_plan)
|
|
838
|
+
drift.push({
|
|
839
|
+
code: 'bundle_association_mismatch',
|
|
840
|
+
detail: 'The verified bundle collaboration binding does not match this record and plan.',
|
|
841
|
+
});
|
|
842
|
+
if (!association.source_session.matches_plan)
|
|
843
|
+
drift.push({
|
|
844
|
+
code: 'bundle_source_native_mismatch',
|
|
845
|
+
detail: 'The verified bundle source native session/agent/harness does not match the planned source identity.',
|
|
846
|
+
});
|
|
847
|
+
if (!association.handoff_snapshot.claims_match)
|
|
848
|
+
drift.push({
|
|
849
|
+
code: 'bundle_claim_mismatch',
|
|
850
|
+
detail: `The bundle snapshot claims contradict the intent: ${association.claimMismatches.join('; ')}.`,
|
|
851
|
+
});
|
|
852
|
+
if (!association.handoff_snapshot.inventory_complete)
|
|
853
|
+
drift.push({
|
|
854
|
+
code: 'bundle_inventory_incomplete',
|
|
855
|
+
detail: 'The bundle snapshot does not prove a complete quiescence inventory (claims/pending completeness).',
|
|
856
|
+
});
|
|
857
|
+
if (verified.value.contextIntegrity !== 'complete')
|
|
858
|
+
drift.push({
|
|
859
|
+
code: 'bundle_context_incomplete',
|
|
860
|
+
detail: `The verified bundle context integrity is ${verified.value.contextIntegrity}.`,
|
|
861
|
+
});
|
|
862
|
+
const requestedEvidence = [
|
|
863
|
+
request.evidence.ack_message_id,
|
|
864
|
+
request.evidence.report_message_id,
|
|
865
|
+
].filter((value) => value !== null);
|
|
866
|
+
const evidence = await loadEvidence(task, requestedEvidence);
|
|
867
|
+
if (evidence.overBudget)
|
|
868
|
+
return failure('needs_input', 'evidence_over_budget', `Evidence exceeds the bounded read budget (${MAX_EVIDENCE_FILES} files, ${MAX_EVIDENCE_BYTES} raw bytes).`, 'Inspect the oversized registered message explicitly; it was not parsed.');
|
|
869
|
+
const changed = await assertTaskStoreCurrent(task);
|
|
870
|
+
if (changed !== null)
|
|
871
|
+
return failure('needs_input', changed.code, changed.message, changed.nextAction);
|
|
872
|
+
const evidenceIssues = [];
|
|
873
|
+
const correlations = [];
|
|
874
|
+
for (const message of evidence.messages) {
|
|
875
|
+
const role = message.message_id === request.evidence.ack_message_id ? 'ack' : 'report';
|
|
876
|
+
const entry = evidence.entries.get(message.message_id);
|
|
877
|
+
const index = evidence.indices.get(message.message_id);
|
|
878
|
+
if (entry === undefined || index === undefined)
|
|
879
|
+
continue;
|
|
880
|
+
const correlated = correlateEvidence({
|
|
881
|
+
role,
|
|
882
|
+
message,
|
|
883
|
+
entry,
|
|
884
|
+
index,
|
|
885
|
+
plan,
|
|
886
|
+
record,
|
|
887
|
+
boundary,
|
|
888
|
+
});
|
|
889
|
+
correlations.push(correlated.correlation);
|
|
890
|
+
evidenceIssues.push(...correlated.issues);
|
|
891
|
+
}
|
|
892
|
+
const supplied = new Set(correlations.map((entry) => entry.message_id));
|
|
893
|
+
for (const messageId of requestedEvidence) {
|
|
894
|
+
if (supplied.has(messageId))
|
|
895
|
+
continue;
|
|
896
|
+
const unread = evidence.unreadable.find((entry) => entry.message_id === messageId);
|
|
897
|
+
evidenceIssues.push({
|
|
898
|
+
code: unread === undefined ? 'evidence_missing' : 'evidence_unreadable',
|
|
899
|
+
detail: unread === undefined
|
|
900
|
+
? `Requested evidence ${messageId} was not read into the report.`
|
|
901
|
+
: `Requested evidence ${messageId} is ${unread.code}; it is never treated as valid proof.`,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
const successionEntry = recordedIntent === null
|
|
905
|
+
? null
|
|
906
|
+
: ((record.successions ?? []).find((item) => item.handoff_id === recordedIntent.handoff_id &&
|
|
907
|
+
item.generation === recordedIntent.generation) ?? null);
|
|
908
|
+
const successionCheck = successionEntry === null
|
|
909
|
+
? null
|
|
910
|
+
: validateSuccession(record, successionEntry);
|
|
911
|
+
const successorIssue = successionEntry === null
|
|
912
|
+
? null
|
|
913
|
+
: successionCheck !== null && successionCheck.ok === false
|
|
914
|
+
? `${successionCheck.code}: ${successionCheck.detail}`
|
|
915
|
+
: null;
|
|
916
|
+
const steps = buildSteps({
|
|
917
|
+
intentFound: recordedIntent !== null,
|
|
918
|
+
createBlockers,
|
|
919
|
+
stage,
|
|
920
|
+
prepared: recordedIntent?.prepared != null,
|
|
921
|
+
successionValid: successionCheck !== null && successionCheck.ok,
|
|
922
|
+
successionIssue: successorIssue,
|
|
923
|
+
});
|
|
924
|
+
const blockers = dedupeBlockers([
|
|
925
|
+
...drift,
|
|
926
|
+
...evidenceIssues,
|
|
927
|
+
...steps.flatMap((step) => step.status === 'blocked' || step.status === 'unavailable'
|
|
928
|
+
? step.blockers
|
|
929
|
+
: []),
|
|
930
|
+
]);
|
|
931
|
+
const senderLookup = routeExclusionFor(record, plan.from);
|
|
932
|
+
const recipientLookup = routeExclusionFor(record, plan.to);
|
|
933
|
+
const exclusions = [senderLookup, recipientLookup]
|
|
934
|
+
.flatMap((lookup) => (lookup.exclusion === null ? [] : [lookup.exclusion]))
|
|
935
|
+
.filter((exclusion, index, all) => all.findIndex((candidate) => candidate.handoff_id === exclusion.handoff_id &&
|
|
936
|
+
candidate.generation === exclusion.generation) === index);
|
|
937
|
+
const effective = senderLookup.state === 'unknown' || recipientLookup.state === 'unknown'
|
|
938
|
+
? 'unknown'
|
|
939
|
+
: senderLookup.state === 'excluded' ||
|
|
940
|
+
recipientLookup.state === 'excluded'
|
|
941
|
+
? 'excluded'
|
|
942
|
+
: 'active';
|
|
943
|
+
const planComplete = steps.every((step) => step.status === 'satisfied') &&
|
|
944
|
+
drift.length === 0 &&
|
|
945
|
+
evidenceIssues.length === 0 &&
|
|
946
|
+
association.collaboration.matches_plan &&
|
|
947
|
+
association.source_session.matches_plan &&
|
|
948
|
+
association.handoff_snapshot.claims_match &&
|
|
949
|
+
association.handoff_snapshot.inventory_complete &&
|
|
950
|
+
verified.value.contextIntegrity === 'complete';
|
|
951
|
+
return {
|
|
952
|
+
ok: true,
|
|
953
|
+
value: {
|
|
954
|
+
schema: COLLAB_ROLLOVER_REPORT_SCHEMA,
|
|
955
|
+
action: request.action,
|
|
956
|
+
ok: true,
|
|
957
|
+
task_root: task.taskRoot,
|
|
958
|
+
task_id: record.task_id,
|
|
959
|
+
state_revision: record.state_revision,
|
|
960
|
+
owner_epoch: currentEpoch,
|
|
961
|
+
stage,
|
|
962
|
+
intent: {
|
|
963
|
+
found: recordedIntent !== null,
|
|
964
|
+
intent_id: plan.intent_id,
|
|
965
|
+
generation: plan.generation,
|
|
966
|
+
handoff_id: plan.handoff_id,
|
|
967
|
+
subtask_id: plan.subtask_id,
|
|
968
|
+
attempt: plan.attempt,
|
|
969
|
+
input_revision: plan.input_revision,
|
|
970
|
+
from: plan.from,
|
|
971
|
+
to: plan.to,
|
|
972
|
+
source_native: pinnedSource,
|
|
973
|
+
bundle: recordedIntent?.bundle ?? {
|
|
974
|
+
ref: canonicalRef,
|
|
975
|
+
digest: request.bundle.digest,
|
|
976
|
+
},
|
|
977
|
+
transfer_boundary: boundary,
|
|
978
|
+
successor_native: successorNative,
|
|
979
|
+
},
|
|
980
|
+
handoff: {
|
|
981
|
+
state: handoff?.state ?? null,
|
|
982
|
+
from: handoff?.from ?? null,
|
|
983
|
+
to: handoff?.to ?? null,
|
|
984
|
+
owner_epoch: handoff?.owner_epoch ?? null,
|
|
985
|
+
},
|
|
986
|
+
prepared: {
|
|
987
|
+
recorded: recordedIntent?.prepared != null,
|
|
988
|
+
evidence_ref: recordedIntent?.prepared?.evidence_ref ?? null,
|
|
989
|
+
inventory: recordedIntent?.prepared?.inventory ?? null,
|
|
990
|
+
at: recordedIntent?.prepared?.at ?? null,
|
|
991
|
+
},
|
|
992
|
+
attempt: attempt === null
|
|
993
|
+
? null
|
|
994
|
+
: {
|
|
995
|
+
state: attempt.state,
|
|
996
|
+
assignee: attempt.assignee,
|
|
997
|
+
input_revision: attempt.input_revision,
|
|
998
|
+
latest_attempt: (record.attempts ?? [])
|
|
999
|
+
.filter((item) => item.subtask_id === attempt.subtask_id)
|
|
1000
|
+
.reduce((max, item) => Math.max(max, item.attempt), 0),
|
|
1001
|
+
native: {
|
|
1002
|
+
observation: attempt.native.observation,
|
|
1003
|
+
session_id: attempt.native.session_id,
|
|
1004
|
+
run_id: attempt.native.run_id,
|
|
1005
|
+
runner: attempt.native.runner,
|
|
1006
|
+
matches_expected: (postTransfer ? successorNative : pinnedSource) === null
|
|
1007
|
+
? null
|
|
1008
|
+
: (postTransfer ? successorNative : pinnedSource)
|
|
1009
|
+
.session_id === attempt.native.session_id &&
|
|
1010
|
+
(postTransfer ? successorNative : pinnedSource)
|
|
1011
|
+
.run_id === attempt.native.run_id &&
|
|
1012
|
+
(postTransfer ? successorNative : pinnedSource)
|
|
1013
|
+
.runner === attempt.native.runner,
|
|
1014
|
+
},
|
|
1015
|
+
expected: {
|
|
1016
|
+
assignee: postTransfer ? plan.to : plan.from,
|
|
1017
|
+
input_revision: plan.input_revision,
|
|
1018
|
+
native: (postTransfer ? successorNative : pinnedSource) === null
|
|
1019
|
+
? null
|
|
1020
|
+
: {
|
|
1021
|
+
session_id: (postTransfer
|
|
1022
|
+
? successorNative
|
|
1023
|
+
: pinnedSource).session_id,
|
|
1024
|
+
run_id: (postTransfer ? successorNative : pinnedSource)
|
|
1025
|
+
.run_id,
|
|
1026
|
+
runner: (postTransfer ? successorNative : pinnedSource)
|
|
1027
|
+
.runner,
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
claims: plan.claims.map((reference) => {
|
|
1032
|
+
const claim = (record.claims ?? []).find((item) => item.claim_id === reference.claim_id);
|
|
1033
|
+
return {
|
|
1034
|
+
claim_id: reference.claim_id,
|
|
1035
|
+
holder: claim?.holder ?? null,
|
|
1036
|
+
resource_epoch: claim?.resource_epoch ?? null,
|
|
1037
|
+
expected_resource_epoch: reference.resource_epoch,
|
|
1038
|
+
expected_holder: postTransfer ? plan.to : plan.from,
|
|
1039
|
+
expected_epoch: postTransfer
|
|
1040
|
+
? reference.resource_epoch + 1
|
|
1041
|
+
: reference.resource_epoch,
|
|
1042
|
+
released_at: claim?.released_at ?? null,
|
|
1043
|
+
};
|
|
1044
|
+
}),
|
|
1045
|
+
succession: {
|
|
1046
|
+
recorded: successionEntry !== null,
|
|
1047
|
+
valid: successionCheck !== null && successionCheck.ok,
|
|
1048
|
+
issue: successorIssue,
|
|
1049
|
+
intent_id: successionEntry?.intent_id ?? null,
|
|
1050
|
+
handoff_id: successionEntry?.handoff_id ?? null,
|
|
1051
|
+
generation: successionEntry?.generation ?? null,
|
|
1052
|
+
archived_at: successionEntry?.archived_at ?? null,
|
|
1053
|
+
proof_message_id: successionEntry?.proof.message_id ?? null,
|
|
1054
|
+
proof_digest: successionEntry?.proof.digest ?? null,
|
|
1055
|
+
routing: 'pending',
|
|
1056
|
+
native_archive: 'unavailable',
|
|
1057
|
+
},
|
|
1058
|
+
bundle: {
|
|
1059
|
+
ref: `${request.bundle.continuity_task_id}/${canonicalRef}`,
|
|
1060
|
+
continuity_task_id: request.bundle.continuity_task_id,
|
|
1061
|
+
digest: bundleRead.value.digest,
|
|
1062
|
+
digest_matches: true,
|
|
1063
|
+
pinned: recordedIntent !== null,
|
|
1064
|
+
verified: true,
|
|
1065
|
+
context_integrity: verified.value.contextIntegrity,
|
|
1066
|
+
checkpoint_present: verified.value.checkpointPresent,
|
|
1067
|
+
workspace_identity_matches: verified.value.workspaceIdentityMatches,
|
|
1068
|
+
execution_allowed: false,
|
|
1069
|
+
source_session: association.source_session,
|
|
1070
|
+
collaboration: association.collaboration,
|
|
1071
|
+
handoff_snapshot: association.handoff_snapshot,
|
|
1072
|
+
},
|
|
1073
|
+
drift,
|
|
1074
|
+
steps,
|
|
1075
|
+
blockers,
|
|
1076
|
+
notify_exclusion: {
|
|
1077
|
+
sender: senderLookup,
|
|
1078
|
+
recipient: recipientLookup,
|
|
1079
|
+
exclusions,
|
|
1080
|
+
effective,
|
|
1081
|
+
},
|
|
1082
|
+
capabilities: {
|
|
1083
|
+
record_reducer: 'available',
|
|
1084
|
+
bundle_verification: 'available',
|
|
1085
|
+
notify_exclusion: 'available',
|
|
1086
|
+
host_stop_proof: 'unavailable',
|
|
1087
|
+
native_launch: 'unavailable',
|
|
1088
|
+
native_session_archive: 'unavailable',
|
|
1089
|
+
routing_removal: 'pending',
|
|
1090
|
+
},
|
|
1091
|
+
evidence: {
|
|
1092
|
+
requested: requestedEvidence,
|
|
1093
|
+
supplied: correlations.length,
|
|
1094
|
+
raw_bytes: evidence.rawBytes,
|
|
1095
|
+
over_budget: evidence.overBudget,
|
|
1096
|
+
unreadable: evidence.unreadable,
|
|
1097
|
+
issues: evidenceIssues,
|
|
1098
|
+
correlations,
|
|
1099
|
+
},
|
|
1100
|
+
plan_complete: planComplete,
|
|
1101
|
+
limits: [
|
|
1102
|
+
'read_only_inspection',
|
|
1103
|
+
'no_apply_or_mutation',
|
|
1104
|
+
'host_stop_proof_unavailable',
|
|
1105
|
+
'native_launch_unavailable',
|
|
1106
|
+
'native_session_archive_unavailable',
|
|
1107
|
+
'routing_removal_pending',
|
|
1108
|
+
'bundle_context_is_not_authority',
|
|
1109
|
+
],
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
}
|