@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,659 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readBoundedFile } from './bounded-file.js';
|
|
4
|
+
import { inspectWorkPlan } from './collab-work-plan.js';
|
|
5
|
+
import { contentDigest, messageFile, validateEnvelope, } from '../collab/envelope.js';
|
|
6
|
+
import { messagesDir, RECORD_FILE } from '../collab/store.js';
|
|
7
|
+
import { isTaskRecord } from '../collab/validation.js';
|
|
8
|
+
import { validateWorkPlan, } from '../collab/work-plan.js';
|
|
9
|
+
/**
|
|
10
|
+
* Bounded, read-only recovery analysis over the existing typed task-record, work-plan and
|
|
11
|
+
* registered-message contracts.
|
|
12
|
+
*
|
|
13
|
+
* `inspectRecoveryProjection` reads one consistent persisted snapshot and reuses the shipped bounded
|
|
14
|
+
* work-plan evaluation to project the unresolved message-driven actions a recovery executor would still
|
|
15
|
+
* owe: continue an interrupted accepted attempt, progress a completed report into coordinator review, or
|
|
16
|
+
* observe an ambiguous effect. It NEVER acknowledges, claims, reserves, notifies, starts a model, mutates
|
|
17
|
+
* no file, and grants no authority: it computes an advisory projection and returns it.
|
|
18
|
+
*
|
|
19
|
+
* It is not an executor and not an authorization. Every answer reflects the persisted record at read time
|
|
20
|
+
* only; concurrent mutation (detected by revalidating the record/plan/namespace after all evidence reads),
|
|
21
|
+
* a truncated or refused read, a missing join or an unverifiable registered envelope yields an explicit
|
|
22
|
+
* incomplete/unknown coverage with a complete-evidence prerequisite rather than a quiet completion or an
|
|
23
|
+
* unqualified actionable recommendation.
|
|
24
|
+
*/
|
|
25
|
+
export const COLLAB_RECOVERY_PROJECTION_SCHEMA = 'pappra/collab-recovery-projection/1';
|
|
26
|
+
const MAX_RECORD_BYTES = 16 * 1024 * 1024;
|
|
27
|
+
const MAX_PLAN_BYTES = 256 * 1024;
|
|
28
|
+
const MAX_ENVELOPE_BYTES = 128 * 1024;
|
|
29
|
+
const MAX_REGISTERED_MESSAGES = 4096;
|
|
30
|
+
const MAX_NODES = 512;
|
|
31
|
+
const MAX_ENVELOPE_READS = 1024;
|
|
32
|
+
const LIMITS = Object.freeze([
|
|
33
|
+
'persisted_snapshot_only',
|
|
34
|
+
'advisory_read_only_projection',
|
|
35
|
+
'no_ack_claim_reserve_notify_or_dispatch',
|
|
36
|
+
'not_an_authorization',
|
|
37
|
+
'no_native_query_or_model',
|
|
38
|
+
'r1_does_not_accept_caller_action_evidence',
|
|
39
|
+
]);
|
|
40
|
+
const ROLES = new Set([
|
|
41
|
+
'writer',
|
|
42
|
+
'coordinator',
|
|
43
|
+
'reviewer',
|
|
44
|
+
]);
|
|
45
|
+
const FINAL_RESULT_STATES = new Set([
|
|
46
|
+
'completed',
|
|
47
|
+
'failed',
|
|
48
|
+
'cancelled',
|
|
49
|
+
]);
|
|
50
|
+
/** Routine lifecycle uncertainty that is not an unknown external effect and does not block continuation. */
|
|
51
|
+
const ROUTINE_UNCERTAINTY = new Set([
|
|
52
|
+
'registered_not_started',
|
|
53
|
+
]);
|
|
54
|
+
function fail(reason, message, nextAction = null) {
|
|
55
|
+
return { ok: false, reason, message, next_action: nextAction };
|
|
56
|
+
}
|
|
57
|
+
function isIdentity(value) {
|
|
58
|
+
return (typeof value === 'object' &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
typeof value.agent_id === 'string' &&
|
|
61
|
+
typeof value.session_id === 'string' &&
|
|
62
|
+
typeof value.harness === 'string');
|
|
63
|
+
}
|
|
64
|
+
/** Two identities match on agent id and session id; harness is compared when both carry one. */
|
|
65
|
+
export function idEq(a, b) {
|
|
66
|
+
return (a !== null &&
|
|
67
|
+
a !== undefined &&
|
|
68
|
+
b !== null &&
|
|
69
|
+
b !== undefined &&
|
|
70
|
+
a.agent_id === b.agent_id &&
|
|
71
|
+
a.session_id === b.session_id &&
|
|
72
|
+
(a.harness === undefined ||
|
|
73
|
+
a.harness === null ||
|
|
74
|
+
b.harness === undefined ||
|
|
75
|
+
b.harness === null ||
|
|
76
|
+
a.harness === b.harness));
|
|
77
|
+
}
|
|
78
|
+
function copyIdentity(id) {
|
|
79
|
+
return {
|
|
80
|
+
agent_id: id.agent_id,
|
|
81
|
+
session_id: id.session_id,
|
|
82
|
+
harness: id.harness,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read the task record through the bounded, no-follow regular-file reader anchored at the task namespace,
|
|
87
|
+
* returning a structured refusal instead of throwing on a missing, oversized, redirected or corrupt record.
|
|
88
|
+
*/
|
|
89
|
+
export function readRecordSafe(taskRoot) {
|
|
90
|
+
const file = path.join(taskRoot, RECORD_FILE);
|
|
91
|
+
const read = readBoundedFile(file, MAX_RECORD_BYTES);
|
|
92
|
+
if (!read.ok)
|
|
93
|
+
return { ok: false, code: `record_${read.code}` };
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(read.text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { ok: false, code: 'record_unparseable' };
|
|
100
|
+
}
|
|
101
|
+
if (!isTaskRecord(parsed))
|
|
102
|
+
return { ok: false, code: 'record_invalid' };
|
|
103
|
+
return { ok: true, record: parsed, digest: read.digest };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Pin the task root, its messages directory and two ancestors by device/inode so a same-revision
|
|
107
|
+
* replacement or a redirected task-root/messages namespace is detected across the whole inspection. A
|
|
108
|
+
* symlinked component makes the anchor unavailable (null).
|
|
109
|
+
*/
|
|
110
|
+
export function anchorTaskNamespace(taskRoot) {
|
|
111
|
+
const targets = [
|
|
112
|
+
taskRoot,
|
|
113
|
+
messagesDir(taskRoot),
|
|
114
|
+
path.dirname(taskRoot),
|
|
115
|
+
path.dirname(path.dirname(taskRoot)),
|
|
116
|
+
];
|
|
117
|
+
const entries = [];
|
|
118
|
+
for (const target of targets) {
|
|
119
|
+
try {
|
|
120
|
+
const st = fs.lstatSync(target);
|
|
121
|
+
if (st.isSymbolicLink())
|
|
122
|
+
return null;
|
|
123
|
+
entries.push({ path: target, dev: st.dev, ino: st.ino });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
entries.push({ path: target, dev: -1, ino: -1 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { entries };
|
|
130
|
+
}
|
|
131
|
+
/** True when every pinned namespace entry still resolves to the same device/inode (no redirect). */
|
|
132
|
+
export function verifyTaskNamespace(anchor) {
|
|
133
|
+
if (anchor === null)
|
|
134
|
+
return false;
|
|
135
|
+
for (const entry of anchor.entries) {
|
|
136
|
+
try {
|
|
137
|
+
const st = fs.lstatSync(entry.path);
|
|
138
|
+
if (st.isSymbolicLink())
|
|
139
|
+
return false;
|
|
140
|
+
if (st.dev !== entry.dev || st.ino !== entry.ino)
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
if (entry.dev !== -1 || entry.ino !== -1)
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
/** Read and validate one registered envelope through the bounded no-follow reader. */
|
|
151
|
+
function readEnvelope(taskRoot, messageId) {
|
|
152
|
+
let file;
|
|
153
|
+
try {
|
|
154
|
+
file = messageFile(taskRoot, messageId);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const read = readBoundedFile(file, MAX_ENVELOPE_BYTES);
|
|
160
|
+
if (!read.ok)
|
|
161
|
+
return null;
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(read.text);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
return validateEnvelope(parsed);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The attempt is held by an approval, cancellation or a CURRENT revocation gate. A historical released
|
|
178
|
+
* claim never vetoes a later valid current grant: only a current held claim marked cancel_pending (held by
|
|
179
|
+
* the attempt owner) or, when no current valid grant exists, a pending revocation, gates recovery.
|
|
180
|
+
*/
|
|
181
|
+
function gateReason(attempt, record) {
|
|
182
|
+
if (attempt.state === 'needs_input')
|
|
183
|
+
return attempt.awaiting ? `awaiting_${attempt.awaiting}` : 'needs_input';
|
|
184
|
+
if (typeof attempt.awaiting === 'string' && attempt.awaiting.length > 0)
|
|
185
|
+
return `awaiting_${attempt.awaiting}`;
|
|
186
|
+
if (typeof attempt.cancel_requested_at === 'string')
|
|
187
|
+
return 'cancel_requested';
|
|
188
|
+
if (attempt.ownership === 'cancelled' || attempt.state === 'cancelled')
|
|
189
|
+
return 'cancelled';
|
|
190
|
+
const forSubtask = (record.claims ?? []).filter((c) => c.subtask_id === attempt.subtask_id);
|
|
191
|
+
const currentHeld = forSubtask.filter((c) => c.released_at === null && idEq(c.holder, attempt.assignee));
|
|
192
|
+
for (const claim of currentHeld)
|
|
193
|
+
if (claim.cancel_pending === true)
|
|
194
|
+
return 'claim_cancel_pending';
|
|
195
|
+
if (currentHeld.length === 0) {
|
|
196
|
+
// No current valid grant: a pending revocation on a (now released) owner claim still gates.
|
|
197
|
+
for (const claim of forSubtask)
|
|
198
|
+
if (claim.cancel_pending === true && idEq(claim.holder, attempt.assignee))
|
|
199
|
+
return 'claim_revoked';
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/** Any unresolved non-routine persisted uncertainty for this attempt is an unknown external effect. */
|
|
204
|
+
function unresolvedUncertainty(record, subtaskId, attempt) {
|
|
205
|
+
for (const item of record.uncertainty ?? []) {
|
|
206
|
+
if (item.resolved_at !== null)
|
|
207
|
+
continue;
|
|
208
|
+
if (item.subtask_id === subtaskId &&
|
|
209
|
+
item.attempt === attempt &&
|
|
210
|
+
!ROUTINE_UNCERTAINTY.has(item.kind)) {
|
|
211
|
+
return item.kind;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The newest admissible continuation ANSWER. Admissible means a registered envelope, re-read and
|
|
218
|
+
* digest-verified against the authoritative record registry, that is a current-coordinator -> self reply for
|
|
219
|
+
* exactly this task/subtask/attempt/input/epoch. The registry is in authoritative application order, so the
|
|
220
|
+
* last admissible entry is the newest correction; a recipient match, a foreign sender, a wrong attempt or
|
|
221
|
+
* input, or an unverifiable digest is not permission.
|
|
222
|
+
*/
|
|
223
|
+
function correlatedAnswer(record, registry, taskRoot, self, coord, coordEpoch, subtaskId, attempt, input) {
|
|
224
|
+
let candidate = false;
|
|
225
|
+
let newest = null;
|
|
226
|
+
for (let index = 0; index < registry.length; index += 1) {
|
|
227
|
+
const entry = registry[index];
|
|
228
|
+
if (entry.type !== 'ANSWER' || entry.subtask_id !== subtaskId)
|
|
229
|
+
continue;
|
|
230
|
+
candidate = true;
|
|
231
|
+
if (entry.attempt !== attempt)
|
|
232
|
+
continue;
|
|
233
|
+
if (coord === null || !idEq(entry.from, coord))
|
|
234
|
+
continue;
|
|
235
|
+
if (!idEq(entry.to, self))
|
|
236
|
+
continue;
|
|
237
|
+
if (entry.owner_epoch !== coordEpoch)
|
|
238
|
+
continue;
|
|
239
|
+
const envelope = readEnvelope(taskRoot, entry.message_id);
|
|
240
|
+
if (envelope === null)
|
|
241
|
+
continue;
|
|
242
|
+
if (contentDigest(envelope) !== entry.content_digest)
|
|
243
|
+
continue;
|
|
244
|
+
if (envelope.type !== 'ANSWER' || envelope.message_id !== entry.message_id)
|
|
245
|
+
continue;
|
|
246
|
+
if (envelope.task_id !== record.task_id)
|
|
247
|
+
continue;
|
|
248
|
+
if (envelope.subtask_id !== subtaskId || envelope.attempt !== attempt)
|
|
249
|
+
continue;
|
|
250
|
+
if (!idEq(envelope.to, self) || !idEq(envelope.from, coord))
|
|
251
|
+
continue;
|
|
252
|
+
if (envelope.owner_epoch !== coordEpoch)
|
|
253
|
+
continue;
|
|
254
|
+
const bodyInput = envelope.body?.input_revision;
|
|
255
|
+
if (input !== null && bodyInput !== input)
|
|
256
|
+
continue;
|
|
257
|
+
if (newest === null || index > newest.order)
|
|
258
|
+
newest = {
|
|
259
|
+
message_id: entry.message_id,
|
|
260
|
+
digest: entry.content_digest,
|
|
261
|
+
order: index,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (newest !== null)
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
message_id: newest.message_id,
|
|
268
|
+
digest: newest.digest,
|
|
269
|
+
candidate: true,
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
candidate,
|
|
274
|
+
reason: 'no_correlated_registered_coordinator_answer',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A review action requires the current `applied` owner report for the latest matching attempt/input and its
|
|
279
|
+
* exact registered envelope identity/type/task/owner/recipient/subtask/attempt/input/status/epoch, verified
|
|
280
|
+
* against the record registry and coordinator. A digest matches bytes, not authority; late/superseded/
|
|
281
|
+
* withheld/stale-sender results and any contradiction stay evidence only.
|
|
282
|
+
*/
|
|
283
|
+
function admittedReport(record, registry, taskRoot, owner, coord, coordEpoch, subtaskId, attempt, attemptEpoch, input, latest) {
|
|
284
|
+
if (attemptEpoch !== coordEpoch)
|
|
285
|
+
return { ok: false, candidate: false, reason: 'attempt_epoch_mismatch' };
|
|
286
|
+
if (attempt !== latest)
|
|
287
|
+
return { ok: false, candidate: false, reason: 'attempt_superseded' };
|
|
288
|
+
const result = (record.results ?? []).find((item) => item.subtask_id === subtaskId &&
|
|
289
|
+
item.attempt === attempt &&
|
|
290
|
+
item.classification === 'applied' &&
|
|
291
|
+
FINAL_RESULT_STATES.has(item.status) &&
|
|
292
|
+
(input === null ||
|
|
293
|
+
item.input_revision === null ||
|
|
294
|
+
item.input_revision === input));
|
|
295
|
+
if (!result)
|
|
296
|
+
return { ok: false, candidate: false, reason: 'no_current_applied_report' };
|
|
297
|
+
const entry = registry.find((item) => item.type === 'REPORT' && item.message_id === result.message_id);
|
|
298
|
+
if (!entry)
|
|
299
|
+
return { ok: false, candidate: true, reason: 'report_not_registered' };
|
|
300
|
+
if (entry.subtask_id !== subtaskId ||
|
|
301
|
+
entry.attempt !== attempt ||
|
|
302
|
+
!idEq(entry.from, owner) ||
|
|
303
|
+
(coord !== null && !idEq(entry.to, coord)))
|
|
304
|
+
return { ok: false, candidate: true, reason: 'report_entry_mismatch' };
|
|
305
|
+
const envelope = readEnvelope(taskRoot, entry.message_id);
|
|
306
|
+
if (envelope === null)
|
|
307
|
+
return { ok: false, candidate: true, reason: 'report_envelope_unreadable' };
|
|
308
|
+
if (contentDigest(envelope) !== entry.content_digest)
|
|
309
|
+
return { ok: false, candidate: true, reason: 'report_digest_mismatch' };
|
|
310
|
+
const bodyStatus = envelope.body
|
|
311
|
+
?.status;
|
|
312
|
+
const bodyInput = envelope.body
|
|
313
|
+
?.input_revision;
|
|
314
|
+
if (envelope.type !== 'REPORT' ||
|
|
315
|
+
envelope.message_id !== result.message_id ||
|
|
316
|
+
envelope.task_id !== record.task_id ||
|
|
317
|
+
envelope.subtask_id !== subtaskId ||
|
|
318
|
+
envelope.attempt !== attempt ||
|
|
319
|
+
!idEq(envelope.from, owner) ||
|
|
320
|
+
(coord !== null && !idEq(envelope.to, coord)) ||
|
|
321
|
+
typeof bodyStatus !== 'string' ||
|
|
322
|
+
!FINAL_RESULT_STATES.has(bodyStatus) ||
|
|
323
|
+
(input !== null && bodyInput !== input))
|
|
324
|
+
return { ok: false, candidate: true, reason: 'report_envelope_mismatch' };
|
|
325
|
+
return {
|
|
326
|
+
ok: true,
|
|
327
|
+
message_id: entry.message_id,
|
|
328
|
+
digest: entry.content_digest,
|
|
329
|
+
candidate: true,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
export async function inspectRecoveryProjection(options) {
|
|
333
|
+
const opts = options ?? {};
|
|
334
|
+
const { taskRoot, planFile, self, policy } = opts;
|
|
335
|
+
if (typeof taskRoot !== 'string' || !path.isAbsolute(taskRoot))
|
|
336
|
+
return fail('task_root_invalid', 'taskRoot must be an absolute path.');
|
|
337
|
+
if (typeof planFile !== 'string' || !path.isAbsolute(planFile))
|
|
338
|
+
return fail('plan_file_invalid', 'planFile must be an absolute path.');
|
|
339
|
+
if (!isIdentity(self))
|
|
340
|
+
return fail('self_identity_invalid', 'self must be a full identity with agent_id, session_id and harness.');
|
|
341
|
+
if (typeof policy !== 'object' ||
|
|
342
|
+
policy === null ||
|
|
343
|
+
!ROLES.has(policy.role) ||
|
|
344
|
+
(policy.authorizedSubtasks !== undefined &&
|
|
345
|
+
!Array.isArray(policy.authorizedSubtasks)))
|
|
346
|
+
return fail('policy_invalid', 'policy.role must be writer, coordinator or reviewer with an optional authorizedSubtasks array.');
|
|
347
|
+
const notes = [];
|
|
348
|
+
let incomplete = false;
|
|
349
|
+
const markIncomplete = (note) => {
|
|
350
|
+
incomplete = true;
|
|
351
|
+
notes.push(note);
|
|
352
|
+
};
|
|
353
|
+
// Anchored, structured initial record read.
|
|
354
|
+
const namespace = anchorTaskNamespace(taskRoot);
|
|
355
|
+
if (namespace === null)
|
|
356
|
+
markIncomplete('the task namespace (task root / messages / ancestors) is unavailable or redirected through a symlink.');
|
|
357
|
+
const initial = readRecordSafe(taskRoot);
|
|
358
|
+
if (!initial.ok)
|
|
359
|
+
return fail('record_unreadable', `The task record could not be read safely (${initial.code}).`, 'Verify the task root and that task.json is a readable regular file.');
|
|
360
|
+
const record = initial.record;
|
|
361
|
+
const revisionInitial = record.state_revision;
|
|
362
|
+
let report;
|
|
363
|
+
try {
|
|
364
|
+
report = await inspectWorkPlan({ taskRoot, planFile });
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return fail('work_plan_inspection_failed', 'The bounded work-plan inspection threw unexpectedly.', 'Re-run against a readable plan and task record.');
|
|
368
|
+
}
|
|
369
|
+
if (report.stale)
|
|
370
|
+
markIncomplete('the work-plan evaluation is stale against the current record; no consistent snapshot.');
|
|
371
|
+
if (report.status !== 'evaluated' || report.core === null)
|
|
372
|
+
markIncomplete(`work-plan status is ${report.status}; evaluation unavailable.`);
|
|
373
|
+
if (report.evidence.over_budget)
|
|
374
|
+
markIncomplete('the work-plan evidence exceeded its bounded read budget.');
|
|
375
|
+
// Plan nodes for the node_id -> assignment_id join (bounded, validated).
|
|
376
|
+
let plan = null;
|
|
377
|
+
const planReadInitial = readBoundedFile(planFile, MAX_PLAN_BYTES);
|
|
378
|
+
const planDigestInitial = planReadInitial.ok ? planReadInitial.digest : null;
|
|
379
|
+
if (!planReadInitial.ok) {
|
|
380
|
+
markIncomplete(`the plan file is not a bounded readable regular file (${planReadInitial.code}).`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
if (report.plan.digest !== null &&
|
|
384
|
+
planReadInitial.digest !== report.plan.digest)
|
|
385
|
+
markIncomplete('the plan digest changed between reads; no consistent snapshot.');
|
|
386
|
+
let parsed = null;
|
|
387
|
+
try {
|
|
388
|
+
parsed = JSON.parse(planReadInitial.text);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
parsed = null;
|
|
392
|
+
}
|
|
393
|
+
const validation = parsed === null ? null : validateWorkPlan(parsed);
|
|
394
|
+
if (validation === null || !validation.ok) {
|
|
395
|
+
markIncomplete('the plan did not validate for the node-to-assignment join.');
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
plan = validation.plan;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const coord = record.coordinator;
|
|
402
|
+
const coordEpoch = coord?.owner_epoch ?? null;
|
|
403
|
+
// Role separation: only an explicit coordinator role whose self is the current record coordinator acts
|
|
404
|
+
// on review work. A writer or reviewer never receives coordinator review-dispatch.
|
|
405
|
+
const isCurrentCoordinator = coord !== null && idEq(self, coord);
|
|
406
|
+
const coordinatorActs = policy.role === 'coordinator' && isCurrentCoordinator;
|
|
407
|
+
if (policy.role === 'coordinator' && !isCurrentCoordinator)
|
|
408
|
+
notes.push('self is not the current record coordinator (agent/session/harness); coordinator review actions are withheld.');
|
|
409
|
+
const registry = record.messages ?? [];
|
|
410
|
+
if (registry.length > MAX_REGISTERED_MESSAGES)
|
|
411
|
+
markIncomplete(`registered message count ${registry.length} exceeds the bounded scan ${MAX_REGISTERED_MESSAGES}.`);
|
|
412
|
+
const boundedRegistry = registry.slice(0, MAX_REGISTERED_MESSAGES);
|
|
413
|
+
const authorized = policy.authorizedSubtasks ?? null;
|
|
414
|
+
const inScope = (sub) => authorized === null || authorized.includes(sub);
|
|
415
|
+
const latestAttempt = (sub) => (record.attempts ?? [])
|
|
416
|
+
.filter((a) => a.subtask_id === sub)
|
|
417
|
+
.reduce((max, a) => Math.max(max, a.attempt ?? 0), 0);
|
|
418
|
+
const planNodes = plan?.nodes ?? [];
|
|
419
|
+
const byNodeId = new Map(planNodes.map((n) => [n.node_id, n]));
|
|
420
|
+
const evalNodes = report.status === 'evaluated' && report.core !== null
|
|
421
|
+
? report.core.nodes
|
|
422
|
+
: [];
|
|
423
|
+
if (evalNodes.length > MAX_NODES)
|
|
424
|
+
markIncomplete(`evaluation node count ${evalNodes.length} exceeds the bound ${MAX_NODES}.`);
|
|
425
|
+
// Join completeness check: an eval or plan node without its counterpart is a join gap.
|
|
426
|
+
if (plan !== null) {
|
|
427
|
+
const evalIds = new Set(evalNodes.map((n) => n.node_id));
|
|
428
|
+
for (const en of evalNodes)
|
|
429
|
+
if (!byNodeId.has(en.node_id))
|
|
430
|
+
markIncomplete(`evaluation node ${en.node_id} has no plan node (join gap).`);
|
|
431
|
+
for (const pn of planNodes)
|
|
432
|
+
if (!evalIds.has(pn.node_id))
|
|
433
|
+
markIncomplete(`plan node ${pn.node_id} has no evaluation entry (join gap).`);
|
|
434
|
+
}
|
|
435
|
+
const actions = [];
|
|
436
|
+
const epochKey = coordEpoch ?? 0;
|
|
437
|
+
let envelopeReads = 0;
|
|
438
|
+
for (const en of evalNodes.slice(0, MAX_NODES)) {
|
|
439
|
+
const pn = byNodeId.get(en.node_id);
|
|
440
|
+
if (!pn)
|
|
441
|
+
continue;
|
|
442
|
+
const sub = pn.assignment_id;
|
|
443
|
+
const att = pn.attempt;
|
|
444
|
+
const input = pn.input_revision ?? null;
|
|
445
|
+
if (!inScope(sub))
|
|
446
|
+
continue;
|
|
447
|
+
const attempt = (record.attempts ?? []).find((a) => a.subtask_id === sub && a.attempt === att) ?? null;
|
|
448
|
+
const evidence = [`workplan:${en.node_id}->${sub}#${att}:${en.status}`];
|
|
449
|
+
if (en.status === 'blocked') {
|
|
450
|
+
actions.push({
|
|
451
|
+
key: `blocked_awaiting_gate:${sub}:${att}:${epochKey}`,
|
|
452
|
+
kind: 'blocked_awaiting_gate',
|
|
453
|
+
subtask_id: sub,
|
|
454
|
+
attempt: att,
|
|
455
|
+
prerequisites: (en.blockers ?? []).map((b) => b.code),
|
|
456
|
+
evidence,
|
|
457
|
+
status: 'blocked',
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (en.status === 'accepted') {
|
|
462
|
+
actions.push({
|
|
463
|
+
key: `verified_completion:${sub}:${att}:${epochKey}`,
|
|
464
|
+
kind: 'verified_completion',
|
|
465
|
+
subtask_id: sub,
|
|
466
|
+
attempt: att,
|
|
467
|
+
prerequisites: [],
|
|
468
|
+
evidence,
|
|
469
|
+
status: 'complete',
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (en.status === 'ready')
|
|
474
|
+
continue;
|
|
475
|
+
if (!attempt) {
|
|
476
|
+
markIncomplete(`node ${en.node_id} (${sub}#${att}) has no matching record attempt (join gap).`);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const ambiguous = unresolvedUncertainty(record, sub, att);
|
|
480
|
+
if (en.status === 'awaiting_acceptance') {
|
|
481
|
+
if (!coordinatorActs)
|
|
482
|
+
continue;
|
|
483
|
+
if (ambiguous !== null) {
|
|
484
|
+
actions.push(ambiguousAction(sub, att, epochKey, evidence, ambiguous));
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (envelopeReads >= MAX_ENVELOPE_READS) {
|
|
488
|
+
markIncomplete('registered-envelope read budget exceeded.');
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
envelopeReads += 1;
|
|
492
|
+
const admit = admittedReport(record, boundedRegistry, taskRoot, attempt.assignee, coord, coordEpoch, sub, att, attempt.owner_epoch, input, latestAttempt(sub));
|
|
493
|
+
if (admit.ok) {
|
|
494
|
+
actions.push({
|
|
495
|
+
key: `progress_report_to_review:${admit.message_id}:${admit.digest}:${sub}:${att}:${input ?? '-'}:${epochKey}`,
|
|
496
|
+
kind: 'progress_report_to_review',
|
|
497
|
+
subtask_id: sub,
|
|
498
|
+
attempt: att,
|
|
499
|
+
prerequisites: ['persist_reviewer_dispatch_before_complete'],
|
|
500
|
+
evidence: [
|
|
501
|
+
...evidence,
|
|
502
|
+
`report:${admit.message_id}`,
|
|
503
|
+
`digest:${admit.digest}`,
|
|
504
|
+
],
|
|
505
|
+
status: 'pending',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
actions.push({
|
|
510
|
+
key: `report_unadmitted:${sub}:${att}:${epochKey}`,
|
|
511
|
+
kind: 'report_unadmitted',
|
|
512
|
+
subtask_id: sub,
|
|
513
|
+
attempt: att,
|
|
514
|
+
prerequisites: ['await_current_applied_registered_report'],
|
|
515
|
+
evidence: [...evidence, `reason:${admit.reason ?? 'unadmitted'}`],
|
|
516
|
+
status: 'unknown',
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (en.status === 'running') {
|
|
522
|
+
// Scope to the real record owner, not only the harness-less plan assignee.
|
|
523
|
+
if (policy.role !== 'writer' || !idEq(attempt.assignee, self))
|
|
524
|
+
continue;
|
|
525
|
+
if (ambiguous !== null) {
|
|
526
|
+
actions.push(ambiguousAction(sub, att, epochKey, evidence, ambiguous));
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const gate = gateReason(attempt, record);
|
|
530
|
+
if (gate !== null) {
|
|
531
|
+
actions.push({
|
|
532
|
+
key: `blocked_awaiting_gate:${sub}:${att}:${epochKey}`,
|
|
533
|
+
kind: 'blocked_awaiting_gate',
|
|
534
|
+
subtask_id: sub,
|
|
535
|
+
attempt: att,
|
|
536
|
+
prerequisites: [gate],
|
|
537
|
+
evidence,
|
|
538
|
+
status: 'blocked',
|
|
539
|
+
});
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (att !== latestAttempt(sub)) {
|
|
543
|
+
markIncomplete(`node ${sub}#${att} is not the latest attempt (${latestAttempt(sub)}); superseded skew.`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (envelopeReads >= MAX_ENVELOPE_READS) {
|
|
547
|
+
markIncomplete('registered-envelope read budget exceeded.');
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
envelopeReads += 1;
|
|
551
|
+
const answer = correlatedAnswer(record, boundedRegistry, taskRoot, self, coord, coordEpoch, sub, att, input);
|
|
552
|
+
if (answer.ok) {
|
|
553
|
+
actions.push({
|
|
554
|
+
key: `answer_supersedes_wait:${sub}:${att}:${answer.message_id}:${answer.digest}:${input ?? '-'}:${epochKey}`,
|
|
555
|
+
kind: 'answer_supersedes_wait',
|
|
556
|
+
subtask_id: sub,
|
|
557
|
+
attempt: att,
|
|
558
|
+
prerequisites: [
|
|
559
|
+
'do_not_resume_obsolete_wait',
|
|
560
|
+
'verify_fresh_capability_and_claims',
|
|
561
|
+
],
|
|
562
|
+
evidence: [
|
|
563
|
+
...evidence,
|
|
564
|
+
`answer:${answer.message_id}`,
|
|
565
|
+
`digest:${answer.digest}`,
|
|
566
|
+
],
|
|
567
|
+
status: 'pending',
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
if (answer.candidate)
|
|
572
|
+
notes.push(`an ANSWER for ${sub}#${att} failed registered-envelope correlation (${answer.reason}); insufficient evidence, not a continuation override.`);
|
|
573
|
+
actions.push({
|
|
574
|
+
key: `resume_work:${sub}:${att}:${input ?? '-'}:${epochKey}`,
|
|
575
|
+
kind: 'resume_work',
|
|
576
|
+
subtask_id: sub,
|
|
577
|
+
attempt: att,
|
|
578
|
+
prerequisites: ['verify_fresh_capability_and_claims'],
|
|
579
|
+
evidence,
|
|
580
|
+
status: 'pending',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Whole-inspection revalidation: re-read the record and plan AFTER all evidence reads. Any revision,
|
|
586
|
+
// digest or namespace change makes every affected recommendation non-actionable. Numeric revision match
|
|
587
|
+
// is necessary, not an OS-level atomic fence; the executor must still revalidate before any effect.
|
|
588
|
+
const finalRead = readRecordSafe(taskRoot);
|
|
589
|
+
if (!finalRead.ok)
|
|
590
|
+
markIncomplete(`the task record could not be re-read after evidence reads (${finalRead.code}).`);
|
|
591
|
+
else if (finalRead.record.state_revision !== revisionInitial)
|
|
592
|
+
markIncomplete(`the record revision changed during inspection (${revisionInitial} -> ${finalRead.record.state_revision}); no consistent snapshot.`);
|
|
593
|
+
else if (finalRead.digest !== initial.digest)
|
|
594
|
+
markIncomplete('the task record bytes changed during inspection; no consistent snapshot.');
|
|
595
|
+
const planReadFinal = readBoundedFile(planFile, MAX_PLAN_BYTES);
|
|
596
|
+
if (!planReadFinal.ok)
|
|
597
|
+
markIncomplete(`the plan file became unreadable during inspection (${planReadFinal.code}).`);
|
|
598
|
+
else if (planDigestInitial !== null &&
|
|
599
|
+
planReadFinal.digest !== planDigestInitial)
|
|
600
|
+
markIncomplete('the plan digest changed during inspection; no consistent snapshot.');
|
|
601
|
+
if (!verifyTaskNamespace(namespace))
|
|
602
|
+
markIncomplete('the task namespace (task root / messages / ancestors) changed or was redirected during inspection; no consistent snapshot.');
|
|
603
|
+
const coverage = incomplete
|
|
604
|
+
? 'incomplete'
|
|
605
|
+
: 'complete';
|
|
606
|
+
if (coverage === 'incomplete') {
|
|
607
|
+
for (let i = 0; i < actions.length; i += 1) {
|
|
608
|
+
const a = actions[i];
|
|
609
|
+
if (a.status === 'pending' &&
|
|
610
|
+
(a.kind === 'resume_work' ||
|
|
611
|
+
a.kind === 'answer_supersedes_wait' ||
|
|
612
|
+
a.kind === 'progress_report_to_review')) {
|
|
613
|
+
actions[i] = {
|
|
614
|
+
...a,
|
|
615
|
+
status: 'unknown',
|
|
616
|
+
prerequisites: ['complete_evidence', ...a.prerequisites],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (actions.length === 0) {
|
|
622
|
+
actions.push({
|
|
623
|
+
key: 'none:-:-:0',
|
|
624
|
+
kind: 'none',
|
|
625
|
+
subtask_id: null,
|
|
626
|
+
attempt: null,
|
|
627
|
+
prerequisites: coverage === 'incomplete' ? ['complete_evidence'] : [],
|
|
628
|
+
evidence: [],
|
|
629
|
+
status: coverage === 'incomplete' ? 'unknown' : 'pending',
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
ok: true,
|
|
634
|
+
value: {
|
|
635
|
+
schema: COLLAB_RECOVERY_PROJECTION_SCHEMA,
|
|
636
|
+
self: copyIdentity(self),
|
|
637
|
+
task_root: report.task_root ?? taskRoot,
|
|
638
|
+
task_id: report.task_id ?? record.task_id ?? null,
|
|
639
|
+
plan_digest: report.plan.digest ?? planDigestInitial,
|
|
640
|
+
record_revision: revisionInitial,
|
|
641
|
+
coverage,
|
|
642
|
+
coordinator_verified: coordinatorActs,
|
|
643
|
+
actions,
|
|
644
|
+
notes,
|
|
645
|
+
limits: LIMITS,
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function ambiguousAction(subtaskId, attempt, epochKey, evidence, uncertaintyKind) {
|
|
650
|
+
return {
|
|
651
|
+
key: `ambiguous_effect:${subtaskId}:${attempt}:${epochKey}`,
|
|
652
|
+
kind: 'ambiguous_effect',
|
|
653
|
+
subtask_id: subtaskId,
|
|
654
|
+
attempt,
|
|
655
|
+
prerequisites: ['observe_persisted_uncertainty_before_any_continuation'],
|
|
656
|
+
evidence: [...evidence, `uncertainty:${uncertaintyKind}`],
|
|
657
|
+
status: 'unknown',
|
|
658
|
+
};
|
|
659
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CollaborationRecoveryRequest } from '../schemas/collaboration.js';
|
|
2
|
+
import { type TaskStoreResult } from './collab-task-store.js';
|
|
3
|
+
export declare const COLLAB_RECOVERY_SCHEMA = "pappra/collaboration-recovery/1";
|
|
4
|
+
export type RecoveryAction = 'inspect' | 'reconcile' | 'lock-break';
|
|
5
|
+
export interface LoadedRecoveryRequest {
|
|
6
|
+
readonly request: CollaborationRecoveryRequest;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LockSnapshot {
|
|
10
|
+
readonly status: 'available' | 'unavailable';
|
|
11
|
+
readonly fingerprint: string | null;
|
|
12
|
+
readonly entries: number;
|
|
13
|
+
readonly bytes: number;
|
|
14
|
+
readonly reason: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function loadRecoveryRequest(file: string, expectedAction: 'reconcile' | 'lock-break'): TaskStoreResult<LoadedRecoveryRequest>;
|
|
17
|
+
/**
|
|
18
|
+
* Stable content fingerprint of the task write-lock directory. It hashes only
|
|
19
|
+
* names and bounded regular-file bytes; PID liveness is an inspection
|
|
20
|
+
* observation and is not part of the content identity.
|
|
21
|
+
*/
|
|
22
|
+
export declare function taskLockFingerprint(taskRoot: string): LockSnapshot;
|
|
23
|
+
export declare function inspectRecovery(taskRoot: string): Promise<TaskStoreResult<Record<string, unknown>>>;
|
|
24
|
+
export declare function planRecovery(taskRoot: string, request: CollaborationRecoveryRequest): Promise<TaskStoreResult<Record<string, unknown>>>;
|
|
25
|
+
export declare function applyRecovery(taskRoot: string, request: CollaborationRecoveryRequest): Promise<TaskStoreResult<Record<string, unknown>>>;
|