# Migrating from 0.6.x to 0.7.0

Harn 0.7.0 replaces the implicit `transcript_policy` dict with
first-class [sessions](../sessions.md). Session lifecycle is driven by
imperative builtins, and unknown inputs hard-error instead of silently
no-op'ing.

This guide lists every removed surface with a side-by-side rewrite.

## `transcript_policy` on workflow nodes

The per-node policy dict is gone. Its fields moved to two dedicated
setters plus lifecycle verbs.

### Before (0.6)

```harn,ignore
workflow_set_transcript_policy(graph, "summarize", {
  mode: "reset",
  visibility: "public",
  auto_compact: true,
  compact_threshold: 8000,
  compact_strategy: "truncate",
  keep_last: 6,
})
```

### After (0.7)

```harn
// Shape the node's compaction behavior:
workflow_set_auto_compact(graph, "summarize", {
  auto_compact: true,
  compact_threshold: 8000,
  compact_strategy: "truncate",
  keep_last: 6,
})
workflow_set_output_visibility(graph, "summarize", "public")

// To reset the stage's conversation explicitly before execution,
// open a caller-controlled session and wire it into the node's
// model_policy:
let sid = agent_session_open("summarize-v2")
workflow_set_model_policy(graph, "summarize", {session_id: sid})
agent_session_reset(sid)
```

`mode: "fork"` maps to `agent_session_fork(src, dst?)` called before
`workflow_execute`, wiring the fork id into the node's
`model_policy.session_id`. `mode: "continue"` is the new default — two
stages sharing a `session_id` share a conversation automatically.

## `transcript_id` / `transcript_metadata` on `llm_call`

Both keys were removed. Session id subsumes them.

### Before

```harn
let result = llm_call("hi", nil, {
  transcript_id: "chat-42",
  transcript_metadata: {user: "ada"},
})
```

### After

```harn
// `session_id` is honored by `agent_loop`; `llm_call` is single-shot.
// For conversational continuity, move to agent_loop:
let sid = agent_session_open("chat-42")
let result = agent_loop("hi", nil, {session_id: sid})
```

If you relied on the `transcript_metadata` bag, attach it to the
session via your own store or pass per-call context through the
`metadata` field of injected messages. `transcript_summary` (per-call
summary injection for mid-loop compaction output) is unchanged.

## `transcript` option on `llm_call` / `agent_loop`

Passing a raw transcript dict through the `transcript` option is now a
hard error.

### Before

```harn
let t = transcript()
let result = agent_loop("task", nil, {transcript: t, provider: "mock"})
```

### After

```harn
let sid = agent_session_open()
let result = agent_loop("task", nil, {session_id: sid, provider: "mock"})
// `agent_session_snapshot(sid)` if you want the transcript back as a dict.
```

The loop loads prior messages from the session store as a prefix
before running and persists the final transcript back on exit.

## Lifecycle via dict (`mode: "reset" | "fork"`)

Previously some call sites accepted a lifecycle dict. That pattern is
gone — call the verbs explicitly:

- `mode: "reset"` → `agent_session_reset(id)`
- `mode: "fork"` → `let dst = agent_session_fork(src)` (optionally with a caller-provided `dst` id)
- `mode: "continue"` → no-op; just reuse the same `session_id`

## Subscribers

`CLOSURE_SUBSCRIBERS` (thread-local in `agent_events.rs`) was removed.
Subscribers now live on `SessionState.subscribers`.

- `agent_subscribe(id, cb)` opens the session lazily and appends.
- `agent_session_fork` does **not** copy subscribers — a fork is a
  conversation branch, not an event fanout.
- `clear_session_sinks` only clears external ACP-style sinks now; it no
  longer evicts sessions.

## Unknown-key / unknown-id behavior

A class of silent pass-throughs is now an error:

- Unknown `agent_session_compact` option keys.
- Missing `role` on `agent_session_inject`.
- Negative `keep_last`.
- `reset` / `fork` / `close` / `trim` / `inject` / `length` / `compact`
  called against an unknown session id.

`exists`, `open`, and `snapshot` remain tolerant of unknown ids by
design.

## `agent_loop` terminal status

`max_iterations` reached without a natural break now reports
`status = "budget_exhausted"` (previously `"done"`). If your host keys
off `"done"` to detect "agent is finished," add `"budget_exhausted"` to
the accept list — the loop ran out of rope, not out of work. Daemon
loops in the same condition no longer silently relabel to `"idle"`.

See the [Sessions](../sessions.md) chapter for the full model and the
0.7.0 entry in the [changelog](https://github.com/burin-labs/harn/blob/main/CHANGELOG.md)
for the complete breaking-change list.
