Skip to content

feat(agents): cross-session plan store with file-level locking [Phase 4.2]#67542

Closed
100yenadmin wants to merge 16 commits into
openclaw:mainfrom
electricsheephq:phase4/cross-session-plans
Closed

feat(agents): cross-session plan store with file-level locking [Phase 4.2]#67542
100yenadmin wants to merge 16 commits into
openclaw:mainfrom
electricsheephq:phase4/cross-session-plans

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Persistent `PlanStore` for sharing plans across multiple sessions and agents, with file-level exclusive locking and stale-lock cleanup.

Tracking

What this PR does

  • `PlanStore` class: `read()`, `write()`, `lock()`, `mergeSteps()` operations
  • File-level exclusive locking via `O_EXCL` + 10s stale-lock cleanup
  • `StoredPlan` / `StoredPlanStep` types with per-step `updatedBy`/`updatedAt` tracking
  • Atomic writes (write to temp file, then rename) to prevent corruption
  • Namespace validation with path traversal protection

Default behavior

No behavior change when unconfigured. Plans remain session-scoped. Cross-session coordination only activates when `planStore.namespace` is explicitly set.

Path traversal fix

`write()` now validates namespace BEFORE checking plan.namespace mismatch, ensuring security-relevant validation runs first.

Files changed

File Change Tests
`src/agents/plan-store.ts` New — PlanStore class 10 tests
`src/agents/plan-store.test.ts` New — round-trip, locking, merge, traversal Self

Dependencies

What follows

  • Integration with plan mode: approved plans can be persisted for cross-agent handoff
  • Wiring into `update_plan` tool so plans auto-persist when namespace is configured

Phase 4.2 of the GPT 5.4 parity sprint. Adds a PlanStore class for
cross-session task coordination, modeled after Claude Code's Tasks
API concept.

## PlanStore (plan-store.ts)
- read(namespace): loads plan from ~/.openclaw/plans/<ns>/plan.json
- write(namespace, plan): persists plan to disk with auto-mkdir
- lock(namespace): file-level exclusive lock with O_EXCL + 10s
  stale-lock auto-cleanup to prevent deadlocks from crashes
- mergeSteps(): merge incoming steps into existing plan by matching
  step text, appending new steps, preserving existing order.
  Tracks updatedBy (session key) and updatedAt per step.

## Schema (StoredPlan, StoredPlanStep)
- StoredPlanStep: step, status (4 values), activeForm?, updatedBy?,
  updatedAt?
- StoredPlan: namespace, steps[], createdAt, updatedAt

## Tests (8 tests)
- Read/write round-trip + nested directory creation
- Lock acquisition, release, and concurrent contention
- Merge: update by text match, append new, preserve order

Default behavior (no namespace configured): plan is session-scoped,
PlanStore is not used. Cross-session coordination only activates when
planStore.namespace is set in config.

Tracking: #66345, #67523.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78fc4cd8a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a persistent, cross-session PlanStore implementation intended to enable multi-agent plan coordination when a namespace is configured.

Changes:

  • Introduces PlanStore with disk-backed read/write, namespace-scoped locking, and mergeSteps semantics.
  • Implements a simple file-based exclusive lock with stale-lock cleanup.
  • Adds Vitest unit coverage for read/write, locking behavior, and merge semantics.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/agents/plan-store.ts New persistent plan store (JSON on disk) with file lock + merge logic.
src/agents/plan-store.test.ts Unit tests covering read/write roundtrip, locking, and merge behavior.

Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.test.ts
Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts
@greptile-apps

greptile-apps Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds PlanStore, a new cross-session plan persistence class with file-level exclusive locking, atomic writes, strict namespace validation, and realpath-based symlink confinement. Previous review concerns (non-atomic write, path traversal, stale-lock reachability) are addressed.

  • P1 — TypeScript error in test (src/agents/plan-store.test.ts:249): the symlink confinement test passes { steps: [...] } to write(), which is missing the required StoredPlan fields namespace, createdAt, and updatedAt; this fails pnpm tsgo. The fix is to add those fields — the confinement check still throws first so the test assertion remains valid.

Confidence Score: 4/5

Safe to merge after fixing the TypeScript type error in the symlink confinement test — one-line fix, no logic change needed.

One P1 finding: the incomplete object literal passed to write() in the symlink test is a TypeScript error that fails the pnpm tsgo gate, which is a hard landing requirement per CLAUDE.md. All P0/security concerns from prior rounds are resolved. Remaining findings are P2 (stale docstring, duplicate comment).

src/agents/plan-store.test.ts — symlink confinement test at line 249 needs required StoredPlan fields added.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/plan-store.test.ts
Line: 248-250

Comment:
**TypeScript error — incomplete `StoredPlan` fails `pnpm tsgo`**

`{ steps: [...] }` is missing the three required `StoredPlan` fields (`namespace`, `createdAt`, `updatedAt`). TypeScript will reject this with "Type is missing the following properties: namespace, createdAt, updatedAt", failing the `pnpm tsgo` gate. At runtime the call still throws at `confine()` before those fields are read, but the compiler doesn't know that.

```suggestion
        await expect(
          store.write("hostile", {
            namespace: "hostile",
            steps: [{ step: "x", status: "pending" }],
            createdAt: 0,
            updatedAt: 0,
          }),
        ).rejects.toThrow(/escapes base directory/);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/plan-store.ts
Line: 322-325

Comment:
**Stale docstring — says "10s", constant is 60s**

`LOCK_STALE_MS` was bumped to `60_000` ms (line 121) but the JSDoc still says "older than 10s", and the inline comment on line 336 likewise says ">10s". Misleads anyone debugging a stuck lock.

```suggestion
  /**
   * Acquires a file-level lock for a namespace.
   * Returns a release function. Stale locks (older than 60s) are
   * cleaned up opportunistically by the next lock() caller.
   */
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/plan-store.ts
Line: 35-40

Comment:
**Duplicate JSDoc — comment belongs to `sanitizePlanShape`, not `VALID_STEP_STATUSES`**

This block (lines 35–40) is an exact duplicate of the opening two sentences of the real JSDoc at lines 43–55 for `sanitizePlanShape`. As written it currently documents the `VALID_STEP_STATUSES` constant with the wrong description. Remove this block entirely.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "fix(plan-store): #67542 — full per-step ..." | Re-trigger Greptile

Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts Outdated
… errors, lock ownership

- Validate namespace to prevent path traversal (reject "..", absolute paths)
- Atomic write: write to temp file then rename to prevent mid-write corruption
- Fix file handle leak: ensure handle.close() in finally on open errors
- Distinguish ENOENT from parse/permission errors in read() instead of
  swallowing all errors
- Verify lock ownership token before unlinking on release to prevent
  one process from deleting another's lock

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30a981e5cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/plan-store.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6192fa4602

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts
Replace O(n) linear scan with a Set when checking whether incoming
steps already exist, improving merge performance for large plans.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.test.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts
Three fixes from Copilot review:

1. mergeSteps: only set updatedBy when sessionKey is truthy, avoiding
   undefined overwriting existing attribution
2. write(): validate plan.namespace matches the namespace argument
   to prevent accidental mismatches
3. Added tests: path traversal rejection + namespace mismatch in write

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed82cc2ab5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/plan-store.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

Comment thread src/agents/plan-store.test.ts
Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Eva added 2 commits April 16, 2026 17:03
…g in mergeSteps

1. read(): verify plan.namespace matches requested namespace to catch
   file corruption or misrouted reads
2. mergeSteps(): deduplicate incoming steps by tracking appended step
   texts in a Set, preventing duplicate step entries when incoming
   contains the same step text multiple times
Change from truthy check to explicit undefined check so that plans with
no namespace field (backward-compat) pass through, while non-undefined
mismatches still throw a corruption error.
@100yenadmin

Copy link
Copy Markdown
Contributor Author

Merge order: 8/8. See #66345 for the full merge sequence.

Path traversal namespace like '../escape' was caught by the mismatch check
(comparing against plan.namespace) before reaching validateNamespace(),
producing a confusing error message instead of the security-relevant one.
Swap the order so validateNamespace runs first.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88e188de43

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/plan-store.ts Outdated
Comment thread src/agents/plan-store.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
@100yenadmin

Copy link
Copy Markdown
Contributor Author

Plan-mode rollout series — status update

This PR is part of a 10-PR plan-mode rollout. The cumulative state ships locally as OpenClaw 2026.4.15 (3a6ec73) from branch feat/plan-channel-parity (= PR #68441 head).

Series overview + landing order: see #68441 (latest comment).

This PR's role in the series: see the original PR description above.

Live test status: covered by the cumulative install — all behaviors from this PR are verified in production-like usage on Telegram + webchat. No regressions detected since the live install (2026-04-18).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/agents/plan-store.ts
@100yenadmin

Copy link
Copy Markdown
Contributor Author

📋 Architecture & Status — single source of truth (commit 39e63febad)

The plan-mode rollout now has a durable architecture document at docs/plans/PLAN-MODE-ARCHITECTURE.md that survives session compactions and tracks the full series.

Quick reference:

  • This is one of 10 open PRs in the cumulative plan-mode rollout (PR-A through PR-11 internal sprint numbering).
  • Recommended landing order: 5 waves (see ARCHITECTURE.md "Recommended landing waves").
  • The cumulative branch feat/plan-channel-parity is the LIVE install (OpenClaw 2026.4.15 (39e63fe)).
  • All 10 PRs target upstream main; each carries forward the cumulative diff which inflates the file count beyond Greptile's 100-file review cap.

Resolution path for the file-count inflation: land PRs in dependency order so the cumulative diff naturally shrinks as upstream main absorbs them. No PR closure/reopen needed — that would lose review history without solving the structural cumulative-rollout pattern.

Hardening status (review pass 1):

Newly-resolved escalation (commit 39e63febad): the previously-deferred #3104743333 (Codex P2 — update_plan merge-mode sidebar refresh) is now fixed via option C (re-emit merged steps via existing agent_plan_event channel). New AgentPlanEventData.mergedSteps field carries the full structured plan; UI subscribes via new maybeForwardMergedPlanEvent handler.

Current status of THIS PR specifically: see the existing comments + bot reviews above. Triage + reply pass coming in the next sprint.

cc @copilot @greptile-apps @chatgpt-codex-connector

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

To use Codex here, create an environment for this repo.

Comment thread src/agents/plan-store.test.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/agents/plan-store.ts
Comment thread src/agents/plan-store.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/agents/plan-store.ts
Comment on lines +303 to +311
// Atomic write: write to a temp file in the same directory, then rename.
const tmpFile = path.join(dir, `.plan-${crypto.randomBytes(4).toString("hex")}.tmp`);
try {
await fs.writeFile(tmpFile, JSON.stringify(plan, null, 2), {
encoding: "utf-8",
mode: 0o600,
});
await fs.rename(tmpFile, planFile);
} catch (err) {

Copilot AI Apr 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The atomic write path creates tmpFile and writes it via fs.writeFile(tmpFile, ...). This follows symlinks and doesn’t use exclusive create, so an attacker (or another process) could pre-create tmpFile (including as a symlink) and cause the write to clobber an unintended target, and there’s also a low-probability name-collision risk. Prefer opening the temp file with exclusive creation (e.g., open with wx plus O_NOFOLLOW where supported), writing via the returned handle, and then renaming.

Suggested change
// Atomic write: write to a temp file in the same directory, then rename.
const tmpFile = path.join(dir, `.plan-${crypto.randomBytes(4).toString("hex")}.tmp`);
try {
await fs.writeFile(tmpFile, JSON.stringify(plan, null, 2), {
encoding: "utf-8",
mode: 0o600,
});
await fs.rename(tmpFile, planFile);
} catch (err) {
// Atomic write: create a temp file securely in the same directory, write via
// the opened handle, then rename into place.
const tmpFile = path.join(dir, `.plan-${crypto.randomBytes(4).toString("hex")}.tmp`);
let handle: fs.FileHandle | undefined;
try {
handle = await fs.open(
tmpFile,
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
0o600,
);
const stat = await handle.stat();
if (!stat.isFile()) {
throw new Error(`Temporary plan path is not a regular file: ${tmpFile}`);
}
await handle.writeFile(JSON.stringify(plan, null, 2), {
encoding: "utf-8",
});
await handle.close();
handle = undefined;
await fs.rename(tmpFile, planFile);
} catch (err) {
if (handle) {
try {
await handle.close();
} catch {
/* ignore */
}
}

Copilot uses AI. Check for mistakes.
@100yenadmin

Copy link
Copy Markdown
Contributor Author

Closing in favor of consolidated PR #68939.

Rationale: this branch was 734 commits behind upstream/main and the
PR's review feedback was firing against a stale base. Rebased the
full 135-commit feature work onto upstream/main @ v2026.4.19-beta.2
(only 5 conflicts to resolve) and consolidated the 10-PR series into
a single umbrella PR for cleaner bot review + faster maintainer
context-loading.

The full iteration history + decision log lives in
docs/plans/PLAN-MODE-ARCHITECTURE.md on the new branch. All
reviewed-and-resolved threads from this PR are honored — see the
architecture doc for what landed where.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants