-
Notifications
You must be signed in to change notification settings - Fork 62
Description
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:
- Spawn Task →
session: ledger→ writes "phase-1-started" to DB - Spawn Task →
session "Do step 1"→ does the work - Spawn Task →
session: ledger→ writes "phase-1-completed" to DB - Spawn Task →
session: ledger→ writes "phase-2-started" to DB - Spawn Task →
session "Do step 2"→ does the work - 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 sessionsSummary
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:
- Spawn Task →
session: ledger→ writes "phase-1-started" to DB - Spawn Task →
session "Do step 1"→ does the work - Spawn Task →
session: ledger→ writes "phase-1-completed" to DB - Spawn Task →
session: ledger→ writes "phase-2-started" to DB - Spawn Task →
session "Do step 2"→ does the work - 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:
- Does all the work in one continuous context
- Never spawns nested Tasks for
session:statements - 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 neededThis 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:
- Avoid blocks in parallel — inline all statements directly in the parallel loop
- Add explicit instructions — in agent prompts, say "spawn a Task for each session statement"
- Use BATCH_SIZE=1 — run sequentially so the main VM stays in control (defeats parallelism)
None of these are satisfactory.
Related
prose.mdsection "Parallel Execution"prose.mdsection "Complete Execution Algorithm" (step 5: "If do block: invoke block with arguments")state/filesystem.mdsection "Scoped Bindings (Block Invocations)"