Skip to content

Block invocation in parallel contexts appears to break nested sessions #17

@MattKotsenas

Description

@MattKotsenas

Hello! This is my first time using OpenProse, so it's quite possible I've done something incorrect. Any help is greatly appreciated.


I have a hypothesis of what's happening (aided by Opus 4.5 in GitHub Copilot CLI), however I'll try to stick to observable behavior. I've attached the LLM's own reasoning below in a collapsable section.

Repro

I've created a .prose file that essentially looks like this:

agent ledger:
  prompt: "Append events to database immediately"
  skills: ["event-log"]

block process-item(item_id):
  session: ledger
    prompt: "Append 'phase-1-started' for {item_id}"
  
  let step1_result = session "Do step 1"
    prompt: "Process step 1 for {item_id}"
  
  session: ledger
    prompt: "Append 'phase-1-completed' for {item_id}"
  
  session: ledger
    prompt: "Append 'phase-2-started' for {item_id}"
  
  let step2_result = session "Do step 2"
    prompt: "Process step 2 for {item_id}"
  
  session: ledger
    prompt: "Append 'phase-2-completed' for {item_id}"
  
  # ... more phases with interleaved ledger calls ...

# Main execution
let items = ["item-A", "item-B", "item-C"]

parallel for item in items:
  do process-item(item)

The ledger agent is using a local skill that's writing the prompt with some additional metadata to a sqlite db.

Expected behavior

For each item, the VM should execute process-item statement-by-statement:

  1. Spawn Task → session: ledger → writes "phase-1-started" to DB
  2. Spawn Task → session "Do step 1" → does the work
  3. Spawn Task → session: ledger → writes "phase-1-completed" to DB
  4. Spawn Task → session: ledger → writes "phase-2-started" to DB
  5. Spawn Task → session "Do step 2" → does the work
  6. Spawn Task → session: ledger → writes "phase-2-completed" to DB

The database should have events with distinct timestamps showing the progression through each phase.

Actual behavior

I see the main sessions making progress by viewing the files being written, however all ledger updates happen at the end with the same timestamp.

Evidence from real execution:

# Actual work shows 3 distinct operations over time:
10:15:22 - Step 1 completed (file created)
10:28:47 - Step 2 completed (file modified)  
10:52:19 - Step 3 completed (file committed)

# But ledger shows events batched at the same timestamp:
phase-1-completed | 2026-02-01 10:52:19
phase-2-completed | 2026-02-01 10:52:19
phase-3-completed | 2026-02-01 10:52:19

The work happened over ~37 minutes, but ledger events were all written at the end.

Hypothesis

The hypothesis the agent came up with based on introspection is that the VM's semantics are ambiguous, and thus it chose to delegate the entire do process-item block to one agent. This also results in missing state in the filesystem state, as the agent is attempting to make progress, but not adhering to any VM requirements. I've validated that if I remove the parallel keyword I see state and ledger entries like I'd expect.

I don't see this pattern anywhere in the examples, so it's possible that this construction is ambiguous.

Follow up

Am I using prose incorrectly? If there's an idiomatic way to represent the same thing without causing ambiguity, let me know. Perhaps we can update anti-patterns with the correct recommendation. If this is a supported construct, a VM update might be required to make things unambiguous. It's also possible I'm off-base, in which case any redirection is appreciated :). Thanks!

Agent reasoning

Click to expand agent's own reasoning # Bug Report: Block invocation in parallel contexts collapses nested sessions

Summary

When a block containing multiple session: statements is invoked inside a parallel block, the nested sessions are not spawned as separate Tasks. Instead, they get collapsed into a single agent execution, causing intermediate state updates (like ledger writes) to be batched or skipped entirely.

Environment

  • OpenProse VM (prose.md)
  • State management: filesystem + SQLite ledger

Reproduction

Program structure

agent ledger:
  prompt: "Append events to database immediately"
  skills: ["event-log"]

block process-item(item_id):
  session: ledger
    prompt: "Append 'phase-1-started' for {item_id}"
  
  let step1_result = session "Do step 1"
    prompt: "Process step 1 for {item_id}"
  
  session: ledger
    prompt: "Append 'phase-1-completed' for {item_id}"
  
  session: ledger
    prompt: "Append 'phase-2-started' for {item_id}"
  
  let step2_result = session "Do step 2"
    prompt: "Process step 2 for {item_id}"
  
  session: ledger
    prompt: "Append 'phase-2-completed' for {item_id}"
  
  # ... more phases with interleaved ledger calls ...

# Main execution
let items = ["item-A", "item-B", "item-C"]

parallel for item in items:
  do process-item(item)

Expected behavior

For each item, the VM should execute process-item statement-by-statement:

  1. Spawn Task → session: ledger → writes "phase-1-started" to DB
  2. Spawn Task → session "Do step 1" → does the work
  3. Spawn Task → session: ledger → writes "phase-1-completed" to DB
  4. Spawn Task → session: ledger → writes "phase-2-started" to DB
  5. Spawn Task → session "Do step 2" → does the work
  6. Spawn Task → session: ledger → writes "phase-2-completed" to DB

The database should have events with distinct timestamps showing the progression through each phase.

Actual behavior

The VM spawns a single Task for do process-item(item) with a natural language summary like "process this item through all phases." The sub-agent:

  1. Does all the work in one continuous context
  2. Never spawns nested Tasks for session: statements
  3. Writes ledger events in bulk at the end (or not at all)

Result: The database shows all events with the same timestamp, or missing intermediate events entirely.

Evidence from real execution:

# Actual work shows 3 distinct operations over time:
10:15:22 - Step 1 completed (file created)
10:28:47 - Step 2 completed (file modified)  
10:52:19 - Step 3 completed (file committed)

# But ledger shows events batched at the same timestamp:
phase-1-completed | 2026-02-01 10:52:19
phase-2-completed | 2026-02-01 10:52:19
phase-3-completed | 2026-02-01 10:52:19

The work happened over ~37 minutes, but ledger events were all written at the end.

Root cause

The VM spec (prose.md) doesn't specify what prompt to give a sub-agent when invoking a block that contains nested session statements.

Current spec says:

  • If parallel: spawn all branches, await per strategy
  • If do block: invoke block with arguments

But it doesn't say:

  • Should the block be expanded inline (VM stays in control)?
  • Should the block be delegated to a sub-agent?
  • If delegated, should the sub-agent execute as a sub-VM or interpret freely?

The ambiguity: When the VM implementor (the LLM) encounters do process-repo(repo) inside a parallel, it has two valid interpretations:

Interpretation Behavior Result
Natural language delegation "Process this repo" → sub-agent does everything Nested sessions collapsed
Sub-VM delegation Pass full prose → sub-agent spawns nested Tasks Nested sessions honored

The spec doesn't mandate either approach, so the VM implementor chooses the simpler one (natural language), which breaks programs that depend on intermediate session execution.

Proposed fix

Add explicit semantics for block invocation in parallel contexts.

Option A: Sub-VM delegation (recommended)

Add to prose.md under "Parallel Execution" or as a new "Block Invocation Semantics" section:

### Block Invocation in Parallel Contexts

When `do block(args)` appears inside a `parallel` block, the VM must spawn 
a Task that executes the block as a **sub-VM**, not as a natural language 
interpretation.

The Task prompt must include:

1. **The full prose of the block** — not a summary
2. **Sub-VM instructions** — "Execute each statement in order. For each 
   `session:` statement, spawn a Task."
3. **State paths** — run directory, execution ID for scoped bindings
4. **Bound arguments** — values for block parameters

Example prompt to sub-agent:

You are a sub-VM executing this block. Execute statement-by-statement.

For each session: or session: agent statement:

  • Spawn a Task (do not do the work inline)
  • Wait for the result
  • Continue to the next statement

Block prose:
```prose
block process-item(item_id):
session: ledger
prompt: "Append 'started' for {item_id}"
let result = session "Do work"
session: ledger
prompt: "Append 'completed' for {item_id}"
```

Run path: .prose/runs/20260201-174628-adc7b5/
Execution ID: 47
Arguments: item_id = "item-A"

Write state updates to the run path. Return when the block completes.


This ensures nested `session` calls are honored as discrete Task spawns.

Option B: Inline expansion for parallel blocks

Alternative: The VM could expand blocks inline rather than delegating:

### Inline Block Expansion

When executing `parallel for item in items: do block(item)`, the VM 
expands the block inline for each iteration. The parallel execution 
happens at the statement level within the block, not at the block level.

This means:
- The main VM stays in control
- Each `session:` inside the block spawns its own Task
- No sub-VM is needed

This is simpler but may have performance implications for deeply nested blocks.

Option C: Explicit syntax

Add syntax to let the program author choose:

# Delegate as sub-VM (default for blocks with sessions)
parallel for repo in repos:
  do process-repo(repo) as vm

# Delegate as natural language (for simple blocks)
parallel for repo in repos:
  do process-repo(repo) as task

Impact

Programs that rely on intermediate state updates (logging, ledgers, checkpoints, progress tracking) will silently fail to record intermediate states when blocks are invoked in parallel contexts.

This is particularly problematic for:

  • Append-only ledgers / audit logs
  • Progress tracking UIs
  • Resumable workflows that checkpoint after each phase
  • Debugging/observability

Workarounds

Until fixed, program authors can:

  1. Avoid blocks in parallel — inline all statements directly in the parallel loop
  2. Add explicit instructions — in agent prompts, say "spawn a Task for each session statement"
  3. Use BATCH_SIZE=1 — run sequentially so the main VM stays in control (defeats parallelism)

None of these are satisfactory.

Related

  • prose.md section "Parallel Execution"
  • prose.md section "Complete Execution Algorithm" (step 5: "If do block: invoke block with arguments")
  • state/filesystem.md section "Scoped Bindings (Block Invocations)"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions