iop/agent-task/m-iop-owned-single-request-agent-execution/13+12_workspace_cleanup/PLAN-cloud-G09.md

15 KiB

Request-owned Workspace Cleanup

For the Implementing Agent

Do not start until packet 12 has complete.log. Implement only request-owned process/artifact cleanup in the listed files, preserve user results, run every verification command, and fill CODE_REVIEW-cloud-G10.md. Do not broaden cleanup into rollback or own official review state.

Background

The internal loop can open a Node workspace and run processes, but success/error/cancel paths do not yet converge on one cleanup owner. This packet adds exactly-once coordinator cleanup and Node reclamation limited to active command groups and registered Node-owned artifacts under .iop/job/<request_id>.

Archive Evidence Snapshot

  • The first-pass pair is preserved at agent-task/m-iop-owned-single-request-agent-execution/13+12_workspace_cleanup/plan_cloud_G09_0.log and agent-task/m-iop-owned-single-request-agent-execution/13+12_workspace_cleanup/code_review_cloud_G10_0.log; it contains no implementation evidence or review verdict.
  • Self-review found that blind os.Root.RemoveAll can cross a mounted subtree and cannot distinguish Node-owned artifacts from injected/unowned entries. Plan 1 uses the immutable request_id, an in-memory ownership inventory, no-follow descriptor traversal, and deepest-first non-recursive removal that fails closed on any ownership or filesystem-boundary mismatch.

Analysis

Files Read

  • AGENTS.md
  • agent-ops/rules/project/rules.md
  • agent-ops/rules/common/rules-roadmap.md
  • agent-ops/rules/common/rules-agent-spec.md
  • agent-ops/rules/project/domain/edge/rules.md
  • agent-ops/rules/project/domain/node/rules.md
  • agent-test/local/rules.md
  • agent-test/local/edge-smoke.md
  • agent-test/local/node-smoke.md
  • agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/iop-owned-single-request-agent-execution.md
  • agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md
  • agent-task/m-iop-owned-single-request-agent-execution/03+02_single_request_coordinator/PLAN-local-G07.md
  • apps/edge/internal/service/service.go
  • apps/node/internal/node/cancel_handler.go
  • apps/node/internal/node/node.go
  • agent-contract/inner/edge-node-runtime-wire.md
  • agent-spec/runtime/edge-node-execution.md

SDD Criteria

  • S07 requires success, error, and cancel to remove request-owned processes and registered artifacts beneath the exact .iop/job/<request_id> namespace while preserving requested workspace results.
  • finalizing waits for cleanup before committing the final terminal. Cleanup never rolls back user files.
  • S11 caller disconnect must cancel provider/tool processes and perform bounded cleanup without another external request.

Verification Context

  • Packet 09 already defines typed cleanup; packets 10/11 own request contexts and process groups; packet 12 owns the coordinator loop.
  • Race tests use temporary roots and helper processes. No external Mac runner is required for logic, while Darwin compile remains a final gate.
  • Existing route-01 cleanup is caller-artifact state and is not reusable as Node filesystem ownership.

State and Concurrency Findings

  • Success, executor error, request timeout, caller cancel, terminal write failure, and duplicate callbacks may all race to cleanup; only one wire cleanup is sent and all callers observe its result.
  • Node cleanup cancels every active process for exactly one request, waits boundedly, removes only registered Node-owned internal artifacts without recursive traversal, closes the request context, and caches a bounded idempotent result.
  • Cleanup failure converts a pending success to failure; existing failure/cancel remains primary while safe cleanup failure is internal evidence.

Test Coverage Gaps

  • No Node operation removes request job artifacts or all request process groups.
  • Coordinator finalization does not wait for workspace cleanup or prove exactly-once across terminal races.

Symbol References

  • Add an optional lifecycle interface to packet 12's internal tool executor; do not break fakes that never opened a workspace.
  • Provider CancelRun and route-01 cleanup remain separate.

Split Judgment

  • Node cleanup and coordinator exactly-once finalization form one end-to-end ownership invariant and must be reviewed together.
  • Observation is packet 14 because it can instrument the stable lifecycle without changing cleanup outcomes.

Scope Rationale

  • Include job namespace, process cancellation/wait, idempotence, coordinator deferral, user-file preservation, tests, wire contract/spec.
  • Exclude general rollback, git reset, arbitrary directory cleanup, metrics/logging implementation, and real provider smoke.

Final Routing

  • evaluation_mode=isolated-reassessment; review_rework_count=0; evidence_integrity_failure=false; build closures true, scores 2/2/2/1/2 = G09.
  • Finalizer route grade-boundary, lane cloud, filename PLAN-cloud-G09.md; risks temporal_state, concurrent_consistency, boundary_contract, variant_product (4).
  • Review scores 2/2/2/2/2 = G10; official filename CODE_REVIEW-cloud-G10.md; no capability/recovery gap.

Dependencies and Execution Order

  1. Require packet 12 completion.
  2. Implement Node cleanup and race tests.
  3. Add optional Edge lifecycle cleanup and terminal ordering.
  4. Synchronize contract/spec only after end-to-end verification.

Implementation Checklist

  • Create and validate only .iop/job/<request_id> from the immutable coordinator identity, inventory every Node-owned artifact, and preserve every user or unowned result.
  • Cancel/wait all process groups and remove only inventoried artifacts plus empty owned directories exactly once per request with bounded concurrent/idempotent result ownership.
  • Make coordinator success/error/cancel/disconnect paths converge on one typed cleanup before terminal commit, with fail-closed success handling.
  • Prove cleanup races, symlink/mount/unowned-entry refusal, user-result preservation, cross-request isolation, failure handling, and synchronize cleanup contract/spec claims.
  • Run dependency, focused race, package, vet, Darwin compile, documentation, and whitespace verification.
  • Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

Implementation Plan

[API-1] Reclaim only Node request-owned state

Problem

  • Packet 11 tracks commands per immutable request identity but has no all-process cleanup or artifact namespace reclamation.
  • apps/node/internal/node/cancel_handler.go:11 owns provider run cancel only.

Solution

Add cleanup.go to workspace runtime. On request open, derive .iop/job/<validated-request-id> only from packet 10's immutable coordinator identity. Create each internal directory component through a no-follow descriptor helper, verify it remains on the admitted filesystem, and register the exact directory identity in the request's in-memory ownership inventory. Every later Node-owned internal artifact must be created through the same helper and registered by relative path, type, and stable file identity; caller file tools still cannot access .iop.

Cleanup atomically elects one owner, cancels all process groups for that request, and waits up to the admitted cleanup bound. It then compares a no-follow enumeration of the exact job tree with the inventory, rejects symlinks, special files, mount/device changes, identity replacements, and unregistered entries, and removes registered files followed by deepest-first empty directories with descriptor-relative non-recursive unlink/rmdir. Never call Root.RemoveAll, never follow an entry during validation/removal, and never widen the path after an error. A mismatch returns typed cleanup failure and preserves the suspect subtree for operator inspection. Concurrent/duplicate callers wait for and receive the same typed result. Retain completed results in a bounded cache; eviction may repeat an idempotent missing-directory check but never broadens scope. Runtime close invokes the same primitive for active requests.

Modified Files and Checklist

  • apps/node/internal/workspace/runtime.go — request lifecycle state, immutable request artifact inventory, and bounded completed-cleanup cache.
  • apps/node/internal/workspace/cleanup.go — process cancel/wait, inventory validation, deepest-first non-recursive removal, and idempotent result ownership.
  • apps/node/internal/workspace/cleanup_path_unix.go — Darwin/Linux descriptor-relative no-follow mkdir/enumerate/identity/unlink/rmdir primitives.
  • apps/node/internal/workspace/cleanup_path_other.go — explicit unsupported fallback outside Darwin/Linux.
  • apps/node/internal/workspace/cleanup_test.go — success/error/cancel races, duplicate calls, active child, timeout, symlink/mount/identity replacement/unowned entry, user-result/cross-request preservation, and invalid id.

Test Strategy

  • Create user results beside .iop, two requests, nested artifacts only through the internal ownership helper, and helper-process descendants; race cleanup/cancel/close and assert only the inventoried target paths disappear. Inject a sibling job, unregistered file, symlink, replaced inode, special file, and mount/substitute where supported; assert cleanup fails closed without deleting the suspect, user, or sibling content. Skip only the privileged mount fixture when unavailable while retaining deterministic filesystem-device mismatch coverage.

Verification

  • go test -race ./apps/node/internal/workspace -run 'TestWorkspaceCleanup' -count=1
  • Expected: exactly one cleanup result owns all registered target resources; suspect/unowned entries fail closed; no user/foreign request file, mounted content, or process is touched.

[API-2] Complete typed cleanup handling and coordinator finalization

Problem

  • Packet 10 leaves OnWorkspaceCleanup unsupported.
  • Packet 03's planned finalizing state waits only for endpoint acknowledgement, not workspace cleanup.

Solution

Before (packet 03 state contract):

successful candidate -> finalizing -> endpoint acknowledgement -> completed

After:

successful candidate -> finalizing -> exactly-once workspace cleanup
  -> endpoint acknowledgement -> completed

Implement Node cleanup mapping. Add a separate optional SingleRequestWorkspaceLifecycle interface implemented by packet 12's tool loop (CleanupWorkspace). The coordinator invokes it once on every terminal/cancel path if the workspace was opened and waits within the frozen request deadline. Success plus cleanup failure becomes failed; existing failure/cancel keeps its category while recording only a safe cleanup code. Caller disconnect still cancels and awaits cleanup even when no response can be written.

Modified Files and Checklist

  • apps/node/internal/node/workspace_handler.go — map typed cleanup request/result without raw fields.
  • apps/node/internal/node/workspace_handler_test.go — cleanup identity/status/idempotence and missing-runtime mapping.
  • apps/edge/internal/service/single_request_tool_loop.go — implement optional workspace lifecycle cleanup using packet 09 wire.
  • apps/edge/internal/service/single_request.go — exactly-once cleanup gate before terminal acknowledgement/return.
  • apps/edge/internal/service/single_request_cleanup_test.go — success/error/cancel/write-failure races, one wire call, cleanup failure, unopened workspace.
  • agent-contract/inner/edge-node-runtime-wire.md — define cleanup scope/idempotence/failure and preservation.
  • agent-spec/runtime/edge-node-execution.md — synchronize finalizing order and named evidence.

Test Strategy

  • Use counting lifecycle fakes plus a net-pipe Node cleanup handler. Race terminal candidates/cancel and assert one cleanup, no early completed state, and correct terminal category.

Verification

  • go test -race ./apps/node/internal/node ./apps/edge/internal/service -run 'Test(NodeWorkspaceCleanup|SingleRequestCleanup)' -count=1
  • rg --sort path -n 'cleanup|\.iop/job|user result|finalizing|exactly' agent-contract/inner/edge-node-runtime-wire.md agent-spec/runtime/edge-node-execution.md
  • Expected: Node and coordinator share the exact scoped cleanup/finalization contract.

Modified Files Summary

File Item
apps/node/internal/workspace/runtime.go API-1
apps/node/internal/workspace/cleanup.go API-1
apps/node/internal/workspace/cleanup_path_unix.go API-1
apps/node/internal/workspace/cleanup_path_other.go API-1
apps/node/internal/workspace/cleanup_test.go API-1
apps/node/internal/node/workspace_handler.go API-2
apps/node/internal/node/workspace_handler_test.go API-2
apps/edge/internal/service/single_request_tool_loop.go API-2
apps/edge/internal/service/single_request.go API-2
apps/edge/internal/service/single_request_cleanup_test.go API-2
agent-contract/inner/edge-node-runtime-wire.md API-2
agent-spec/runtime/edge-node-execution.md API-2
agent-task/m-iop-owned-single-request-agent-execution/13+12_workspace_cleanup/CODE_REVIEW-cloud-G10.md API-1, API-2

Final Verification

  1. test -f agent-task/m-iop-owned-single-request-agent-execution/12+05,06,08,11_internal_tool_loop/complete.log || test "$(compgen -G 'agent-task/archive/*/*/m-iop-owned-single-request-agent-execution/12+05,06,08,11_internal_tool_loop/complete.log' | wc -l)" -eq 1
  2. go test -race ./apps/node/internal/workspace -run 'TestWorkspaceCleanup' -count=1
  3. go test -race ./apps/node/internal/node ./apps/edge/internal/service -run 'Test(NodeWorkspaceCleanup|SingleRequestCleanup)' -count=1
  4. go test ./apps/node/internal/workspace ./apps/node/internal/node ./apps/edge/internal/service -count=1
  5. go vet ./apps/node/internal/workspace ./apps/node/internal/node ./apps/edge/internal/service
  6. GOOS=darwin GOARCH=arm64 go test -c -o /tmp/iop-workspace-cleanup-darwin.test ./apps/node/internal/workspace
  7. rg --sort path -n 'cleanup|request_id|\.iop/job|inventory|no-follow|user result|finalizing|exactly' agent-contract/inner/edge-node-runtime-wire.md agent-spec/runtime/edge-node-execution.md
  8. git diff --check

Expected: packet 12 is uniquely complete; cleanup is scoped to immutable request identity and inventoried Node artifacts, refuses symlink/mount/unowned replacement, is bounded and race-free, and precedes terminal completion; Darwin compile and package regressions pass. Cached tests are not acceptable.

After completing all code changes, fill implementation-owned sections in CODE_REVIEW-*-G??.md.