# Workspace and Lifecycle
## 1. Goal
Preserve the Symphony workspace contract while adapting it to OpenHands conversation persistence and local MVP safety constraints.
## 2. Workspace mapping
Each issue maps to exactly one workspace path:
```text
<workspace.root>/<sanitized_issue_identifier>
```
Sanitization rule:
- keep `[A-Za-z0-9._-]`
- replace every other character with `_`
Configuration rule:
- if `workspace.root` is provided through env indirection such as `$WORKSPACE_ROOT`, that env var must resolve during workflow loading; OpenSymphony must not silently fall back to the default workspace root for an explicit operator-supplied path
Examples:
- `ABC-123` -> `ABC-123`
- `feature/42` -> `feature_42`
- `Bug: weird path` -> `Bug__weird_path`
Because this sanitization is not injective, workspace reuse must be gated by the persisted issue manifest for the current path. If an existing current-path manifest claims the same sanitized key for a different issue, OpenSymphony must refuse reuse instead of silently aliasing two issues onto one workspace.
## 3. Hard safety invariants
- The resolved workspace path must stay under `workspace.root`.
- Relative workflow directories must be normalized before resolving relative `workspace.root` values so the resulting workspace root remains absolute for the workspace manager.
- The doctor preflight must use the target repo `WORKFLOW.md` `workspace.root` rather than a duplicate CLI-only workspace root so the repo-owned policy remains authoritative.
- The issue workspace path itself must not be a symlink when OpenSymphony reuses or validates it.
- `cwd` for all hook commands and all OpenHands runs must equal the resolved issue workspace path unless an explicit per-command `cwd` override inside the same workspace is required.
- When `openhands.local_server.command` is omitted, the runtime-owned local tooling layer must resolve the pinned launcher from the OpenSymphony checkout before that `cwd` switch happens. Workflow resolution must not bake a compile-time checkout path into config defaults.
- Non-loopback remote `openhands.transport.base_url` targets must use `https://` and set `openhands.transport.session_api_key_env`.
- Unauthenticated loopback `http://` targets remain eligible for daemon-managed local supervision. When those loopback targets include a path prefix, the runtime normalizes them back to the origin before launching the repo-owned local server and before rebuilding the local client; authenticated or non-loopback targets still remain external.
- Explicit workflow-owned `openhands.local_server.command` overrides are supported only for daemon-managed local supervision; external, authenticated, or `local_server.enabled: false` targets must fail deterministically at the runtime boundary instead of silently ignoring the override.
- Explicit workflow-owned `openhands.local_server.enabled: false` overrides are currently rejected until the runtime supervisor can honor workflow-owned local-server disablement instead of still deciding launch behavior from the localhost base URL plus pinned tooling readiness.
- Explicit workflow-owned `openhands.local_server.env` overrides are currently rejected until the runtime supervisor creation path forwards them into the actual launcher environment instead of still using runtime-owned defaults.
- Explicit workflow-owned `openhands.local_server.readiness_probe_path` overrides are currently rejected until the runtime supervisor launch path copies them into the probe configuration instead of always using `/openapi.json`.
- Explicit workflow-owned `openhands.local_server.startup_timeout_ms` overrides are currently rejected until the runtime supervisor creation path consumes workflow-owned startup timeout settings instead of always using the supervisor default.
- Workflow-owned `openhands.conversation.reuse_policy` values resolve into the runtime-owned issue-session path. The current runtime supports `per_issue` and `fresh_each_run`, persists the active policy in `conversation.json`, and treats any other configured value as an explicit runtime compatibility failure.
- OpenSymphony must never run agent work directly in `workspace.root`.
- Path checks must operate on canonicalized paths when possible.
## 4. Workspace directory layout
Recommended layout inside each issue workspace:
```text
<issue_workspace>/
.opensymphony.after_create.json
.opensymphony/
issue.json
run.json
conversation.json
prompts/
last-full-prompt.md
last-full-prompt.json
last-continuation-prompt.md
last-continuation-prompt.json
runs/
attempt-0001/
prompt-full-001.md
prompt-full-001.json
logs/
worker.log
hook.log
openhands/
create-conversation-request.json
last-conversation-state.json
generated/
issue-context.md
memory-context.md
session-context.json
```
Notes:
- `.opensymphony/` is OpenSymphony-owned metadata.
- `.opensymphony.after_create.json` is an internal OpenSymphony bootstrap receipt written at the workspace root immediately after a successful first-time `after_create` hook and before `.opensymphony/` metadata bootstrap.
- The workspace layer bootstraps `issue.json`, `run.json`, and the supporting metadata directories after a successful first-time `after_create` hook so clone/worktree hooks still see a fresh workspace directory.
- `conversation.json` now uses workspace-owned path and serialization helpers, but the OpenHands issue-session runner still owns when it is created, reused, or reset.
- `conversation.json` is the issue-to-session registry: it stores the issue reference, stable `conversation_id`, active reuse policy, timestamps, transport diagnostics, the launch profile needed to resume the same OpenHands thread later through `opensymphony debug`, and an `llm_config_fingerprint` that tracks the model name for observability (no longer used for drift detection).
- `prompts/` holds the latest prompt of each kind plus JSON metadata that points back to the per-run archive.
- `runs/attempt-####/` holds immutable per-run prompt captures for auditability without mutating repository-owned policy files.
- `generated/memory-context.md` is a deterministic pre-implementation bundle from captured memory when project memory is enabled.
- The repository working tree remains otherwise untouched except by normal agent work.
- OpenSymphony must never overwrite repository-owned `AGENTS.md`.
## 5. Workspace ownership model
The workspace manager owns:
- workspace path resolution
- existence checks
- creation
- optional initial population through hooks
- metadata bootstrap
- cleanup
The OpenHands runtime owns only the conversation execution inside that path.
## 6. Lifecycle hooks
Preserve the Symphony hook model.
## 6.1 `after_create`
Runs once after a brand-new issue workspace is created.
On first bootstrap, this hook runs before OpenSymphony creates `.opensymphony/` so repository bootstrap commands such as `git clone <repo> .` or `git worktree add <path>` can target an otherwise empty workspace directory.
Use for:
- cloning the repo
- adding a git worktree
- bootstrapping local tooling
- creating ignored helper files
Do not rerun it on every worker attempt.
If the first `after_create` attempt fails before bootstrap completes, the next `ensure` attempt should retry `after_create` instead of treating the partially initialized workspace directory as fully reusable.
After a successful first-time `after_create`, OpenSymphony must persist a root-scoped bootstrap receipt before it starts creating `.opensymphony/` metadata. If later bootstrap steps fail, the next `ensure` should resume metadata bootstrap without rerunning `after_create`.
Steady-state workspace ownership is still determined by a decodable OpenSymphony-owned `issue.json` whose workspace path and sanitized key match the current workspace, not by raw file existence. Repository-provided, copied, or undecodable `.opensymphony/issue.json` or `.opensymphony.after_create.json` artifacts must not suppress a required first-bootstrap retry.
## 6.2 `before_run`
Runs before each worker lifetime.
Use for:
- syncing or fetching changes
- checking workspace health
- generating run-scoped metadata
- recording diagnostic info
## 6.3 `after_run`
Runs after each worker lifetime regardless of outcome, best effort.
Run finalization must flow through the workspace manager's `finish_run` path so the final
`run.json` and any generated artifacts include `after_run` hook receipts.
Use for:
- capturing status
- collecting logs
- updating generated context artifacts
- cleaning temporary files
## 6.4 `before_remove`
Runs before workspace deletion for terminal issues.
Use for:
- final log or artifact collection
- archiving evidence
- safe cleanup steps
## 6.5 Hook execution rules
- Hooks execute inside the issue workspace unless explicitly documented otherwise.
- Workspace-handle validation must reject symlinked workspace roots before hook execution, cleanup, or manifest I/O can proceed.
- Any explicit hook `cwd` override must still resolve inside the same issue workspace.
- Containment checks for explicit hook `cwd` overrides should use canonicalized paths so symlinked subdirectories cannot escape the workspace.
- OpenSymphony-managed metadata paths under `.opensymphony/` must reject symlinked directories or files before any manifest read or write.
- Unix hook commands should run via a non-login `sh -c` shell so host profile startup files cannot change `cwd` or fail the hook before the configured command runs.
- Hook timeouts use the configured `hooks.timeout_ms`.
- When a hook times out, OpenSymphony must terminate the entire spawned process tree rather than only the direct shell wrapper process.
- Hook failures are categorized and surfaced with issue context.
- `after_run` and `before_remove` are best effort by default.
- `after_create` and `before_run` failures fail the current worker attempt.
## 7. Issue metadata manifest
Persist a small issue manifest under `.opensymphony/issue.json`.
Suggested fields:
- `issue_id`
- `identifier`
- `title`
- `current_state`
- `sanitized_workspace_key`
- `workspace_path`
- `created_at`
- `updated_at`
- `last_seen_tracker_refresh_at`
Use cases:
- restart recovery
- operator debugging
- startup bootstrap before new worker launch
Restart recovery should rely on managed workspace metadata only:
- `issue.json` restores the owning issue reference and last known tracker state
- `run.json` indicates whether the last known worker lifetime was still in `preparing`, `prepared`, or `running` when the daemon stopped
- startup bootstrap may republish recovered workspace state immediately, but it must not assume any recovered run is still live until a fresh scheduler tick revalidates tracker state and runtime launch status
- workspace introspection
- authoritative ownership check for non-injective sanitized workspace keys
Current repository implementation:
- the scheduler recovery path consumes manifest-derived records that include the normalized issue identity plus the attached workspace record
- recovered active issues reuse that workspace attachment on the next scheduler poll instead of recreating the workspace path
## 7.1 Run metadata manifest
Persist the latest worker-lifetime manifest under `.opensymphony/run.json`.
Suggested fields:
- `run_id`
- `attempt`
- `issue_id`
- `identifier`
- `sanitized_workspace_key`
- `workspace_path`
- `status`
- `status_detail`
- `hooks`
- `created_at`
- `updated_at`
Use cases:
- capture `before_run` and `after_run` hook outcomes with stdout/stderr for diagnostics
- explain the latest worker-lifetime state during restart recovery
- make cleanup and retry decisions inspectable without daemon memory
Current repository note:
- the run manifest is currently explanatory recovery evidence for operators and future adapters; the generic scheduler core already reuses workspace ownership from recovery records, while persisted retry-queue reconstruction remains a later follow-on
## 8. Conversation metadata manifest
Persist `.opensymphony/conversation.json`.
Suggested fields:
- `issue_id`
- `identifier`
- `conversation_id`
- `reuse_policy`
- `server_base_url`
- `transport_target`
- `http_auth_mode`
- `websocket_auth_mode`
- `websocket_query_param_name`
- `persistence_dir`
- `created_at`
- `updated_at`
- `last_attached_at`
- `launch_profile`
- `llm_config_fingerprint`
- `fresh_conversation`
- `workflow_prompt_seeded`
- `reset_reason`
- `runtime_contract_version`
- `last_prompt_kind`
- `last_prompt_at`
- `last_prompt_path`
- `last_execution_status`
- `last_event_id`
- `last_event_kind`
- `last_event_at`
- `last_event_summary`
This file is the bridge between Symphony issue ownership and OpenHands conversation reuse.
It also makes conversational debugging deterministic: `opensymphony debug <issue-id>`
uses the stored issue reference plus `launch_profile` to find the workspace, resume the
same thread, and preserve context across orchestrator restarts or retry attempts.
**Note on LLM config fingerprint**: The `llm_config_fingerprint` field previously tracked
hashed API keys and base URLs for drift detection. This has been simplified to only track
the model name for observability. Conversations are now reused as-is without checking for
config drift, avoiding the complexity and brittleness of the previous delete-and-recreate
approach. OpenSymphony never writes raw LLM API keys or base URLs into `conversation.json`.
## 9. Generated context artifacts
OpenSymphony should generate additive helper files under `.opensymphony/generated/`.
Recommended files:
### `issue-context.md`
Human-readable summary for the agent and operator:
- issue identifier and title
- current state
- last worker outcome
- repository-owned `WORKFLOW.md`, `AGENTS.md`, and optional `.agents/skills/` locations
- important constraints
- known blockers
- location of OpenSymphony metadata files
### `memory-context.md`
Pre-implementation guidance compiled from already captured memory. The runtime
generates it before worker launch when project memory is enabled. It excludes
the current issue capsule because capture happens after completion, and uses
deterministic Linear graph facts plus captured memory buckets instead of
free-form same-milestone search.
### `session-context.json`
Machine-readable runtime summary:
- conversation ID
- reuse policy
- server base URL plus transport/auth diagnostics
- run ID
- attempt number
- worker ID
- prompt kind and prompt artifact path
- whether the workflow prompt has been seeded into the conversation
- last known execution status
- last event summary
- last run ID and status
- last prompt kind and path
- recent validation commands
- last retry reason if any
- latest worker outcome
These files help continuity without altering the repository's own guidance files.
They are additive references to repo-owned policy, not replacements for it.
## 10. Prompt artifacts
Persist the last prompts sent by OpenSymphony.
Why:
- debugging render issues
- replaying a failed run
- comparing full vs continuation prompt logic
- making live tests and regressions easier to inspect
Store at minimum under `.opensymphony/prompts/`:
- `last-full-prompt.md`
- `last-full-prompt.json`
- `last-continuation-prompt.md`
- `last-continuation-prompt.json`
Also archive every captured prompt under `.opensymphony/runs/attempt-####/` using deterministic sequence-numbered file names such as:
- `prompt-full-001.md`
- `prompt-full-001.json`
- `prompt-continuation-001.md`
- `prompt-continuation-001.json`
The stable files in `prompts/` should always mirror the latest capture of that kind, while the per-run `runs/` archive remains append-only for that worker attempt.
Current implementation detail:
- the full workflow prompt is rendered from `WORKFLOW.md`
- continuation guidance is a separate built-in resume prompt, not a rerender of the workflow template
- `conversation.json` records which prompt shape last ran, which reuse policy produced the current conversation, and whether the workflow prompt has been successfully seeded into the reused conversation
- **Simplified resumption**: conversations are reused as-is without checking for LLM config drift. The stored LLM configuration in the conversation's `meta.json` is used without modification.
## 11. Conversation lifetime policy inside the workspace
Supported policies:
- `per_issue`
- one conversation per issue
- conversation persistence is stored under the issue workspace
- reused across worker lifetimes
- retained after worker completion so the issue remains debuggable until workspace cleanup
- `fresh_each_run`
- the issue workspace still keeps the latest `conversation.json` for observability
- every worker lifetime creates a new OpenHands conversation and resends the full workflow prompt
- the next run must not reuse the previous `conversation_id` even if the old manifest is still present locally
Reset handling:
- archive old metadata if useful
- write a reset reason
- reset when the persisted `reuse_policy` no longer matches the current workflow
- **Note**: LLM config drift no longer triggers reset. Conversations are reused as-is with their stored configuration.
- resend full prompt on the next fresh run
## 12. Clean exit and continuation
Symphony requires a short continuation retry after normal worker exit.
OpenSymphony implementation:
- worker may already have run multiple in-process turns on the same conversation
- when the worker finally exits cleanly, the orchestrator schedules the short retry
- the next worker reattaches to the same workspace and usually the same conversation
- because the conversation already contains the original assignment, the next worker sends continuation guidance instead of replaying the full prompt
## 13. Cleanup policy
## 13.1 Terminal issues
When the tracker says an issue is terminal:
- cancel any active worker
- run `before_remove` best effort
- delete the workspace if configured to do so
Keep cleanup policy configurable enough to allow retention during debugging.
## 13.2 Non-active, non-terminal issues
When the tracker says the issue is not active and not terminal:
- cancel active work
- do not delete the workspace by default
- preserve metadata and conversation state for possible later reactivation
## 13.3 Startup sweep
On daemon startup, optionally sweep known workspaces against terminal tracker states and remove those that no longer need to exist.
## 14. Local MVP safety posture
The local MVP assumes host access.
Implications:
- hook commands run on the host
- OpenHands tool execution may run on the host
- workspace root selection matters
- docs must strongly recommend trusted repositories only
A future hosted mode can keep the same workspace ownership model while moving actual execution into a remote or container-backed environment.
## 15. Suggested implementation API
```rust
trait WorkspaceManager {
fn workspace_path_for(&self, issue_identifier: &str) -> Result<PathBuf>;
async fn ensure(&self, issue: &IssueDescriptor) -> Result<EnsureWorkspaceResult>;
async fn start_run(
&self,
workspace: &WorkspaceHandle,
run: &RunDescriptor,
) -> Result<RunManifest>;
async fn finish_run(
&self,
workspace: &WorkspaceHandle,
run: &mut RunManifest,
status: RunStatus,
) -> Result<()>;
async fn cleanup(
&self,
workspace: &WorkspaceHandle,
state: IssueLifecycleState,
) -> Result<CleanupOutcome>;
}
```
`WorkspaceHandle` should expose:
- `issue_id`
- `identifier`
- `workspace_path`
- `metadata_dir`
- `issue_manifest_path`
- `run_manifest_path`
- `conversation_manifest_path`
## 16. Tests required
- sanitize identifier edge cases
- canonical path containment
- create vs reuse
- `after_create` only once
- clone/worktree-compatible fresh bootstrap before `.opensymphony/` exists
- retry `after_create` after a failed first bootstrap
- `before_run` every worker lifetime
- timeout on hook
- hook stderr capture
- canonical `cwd` containment for symlinked subdirectories
- terminal cleanup
- issue and run metadata file write and reload
- conversation reset path preserves workspace safety
## Current model
- COE-287 contributed: PR #48: Add opensymphony debug command for issue conversations (merge `021f5ad`)
## Important invariants
- Preserve the behavior described in the recent captured changes unless current code and tests show it has changed.
- Use capsule source refs to inspect the original PR or Linear issue when context is ambiguous.
## Operational flow
```mermaid
flowchart TD
memory["Captured issue memory"] --> area["Workspace and Lifecycle"]
area --> docs["docs/workspace-and-lifecycle.md"]
```
## Known gotchas
- No area-specific gotchas were inferred from the selected memory.
## Recent changes
- COE-287: Add opensymphony debug command for conversational session debugging
## Source refs
- COE-287