# 지속 파일 schema 참조

<a id="overview"></a>

Norma의 durable primitives는 relational/graph DB가 아니라 JSONL, Markdown과 JSON 파일이다. 이 페이지는 production writer/reader가 실제 사용하는 on-disk shape를 열거한다. 파일 permission은 대부분 `0644`, directory는 `0755`이며 encryption, fsync, cross-process lock, signature는 없다.

<a id="transcript-record"></a>

## Transcript JSONL

main path는 `<dir>/<sessionID>.jsonl`, subagent는 `<dir>/<sessionID>/subagents/agent-<agentID>.jsonl`이다. 한 줄은 `transcript.Record`의 다음 **10개 JSON field**다.

| JSON field | type | presence/meaning |
|---|---|---|
| `uuid` | string | writer가 생성한 128-bit random hex(rare random failure 시 timestamp hex) |
| `parent_uuid` | string | 이전 성공 append record; 첫 record에는 생략 |
| `session_id` | string | writer identity; host 제공 값 검증 없음 |
| `agent_id` | string | sidechain agent id, main에서는 생략 |
| `is_sidechain` | bool | sidechain일 때 true, otherwise 생략 |
| `timestamp` | RFC3339Nano string | UTC write time |
| `type` | string | `message` 또는 `boundary` |
| `message` | `llm.Message` pointer | message record에서 role+content; boundary면 보통 생략 |
| `usage` | `llm.Usage` pointer | zero가 아닌 assistant-turn usage만 |
| `boundary` | `llm.BoundaryMeta` pointer | boundary record만 |

### Nested variants

| nested object | fields/variants |
|---|---|
| `message` | `role`; `content[]` |
| `content[]` common | `type`: text/thinking/tool_use/tool_result/compact_boundary/skill; variant fields는 아래와 같음 |
| text | `text` |
| thinking | `thinking`, optional `signature` |
| tool use | `id`, `name`, raw JSON `input` |
| tool result | `tool_use_id`, nested `content[]`, `is_error` |
| compact boundary | raw `input`이 `BoundaryMeta` object 또는 legacy bare pre-token integer |
| `usage` | `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens` |
| `boundary` | optional `trigger`, required-value `pre_tokens`, optional `messages_summarized`, `active_skills[]` |

writer는 append error의 첫 값을 `Writer.Err()`에 남기고 in-memory loop를 계속한다. loader는 missing file을 empty로 취급하지만 **한 줄이라도 malformed JSON이면 전체 load가 error**이고 corrupt tail repair는 없다. `Messages`는 boundary record와 nil message를 건너뛰고 usage를 더한다. `Session.Resume`은 trailing unanswered assistant tool-use messages를 버리고 새 record를 last UUID 뒤에 잇는다. [record 근거](evidence:transcript-record) [nested message 근거](evidence:llm-types) [boundary 근거](evidence:llm-boundary)

<a id="memory-files"></a>

## Memory Markdown

각 memory는 `<memory-dir>/<slug(name)>.md`, index는 `MEMORY.md`다.

```text
---
name: <unescaped single-line value>
description: <unescaped single-line value>
type: user | feedback | project | reference   # empty면 line 생략
---

<Markdown body>
```

| 항목 | writer/reader semantics |
|---|---|
| slug | name lowercase → ASCII regex `[^a-z0-9]+`를 `-`로 치환 → 앞뒤 `-` 제거; Unicode-only/충돌 name은 empty 또는 같은 path가 될 수 있음 |
| type | 네 상수는 tool prompt convention이며 parser가 unknown value를 거부하지 않음 |
| frontmatter | YAML library를 쓰지 않고 line을 첫 `:`에서 나눔; quoting/escaping/nested YAML 없음 |
| malformed/no frontmatter | 전체 파일을 content로, name은 filename stem fallback |
| scan | `MEMORY.md`와 subdir를 제외한 `.md`; parse/read error file은 조용히 skip; modtime newest first |
| index | Save 뒤 전체 scan으로 rebuild; memory file write와 index write는 transaction이 아님 |
| relevance | ASCII `[a-z0-9_]+` token substring score; name/type 3, description 2, body 1, recency tie-break |

[Memory schema 근거](evidence:memory-schema) [relevance 근거](evidence:memory-relevance)

<a id="noa-state"></a>

## Noa `state.json` v1

path는 `<ArchiveBaseDir>/<SessionID>/state.json`; writer는 같은 directory temp file을 `0644`로 닫고 rename한다.

| object | JSON fields |
|---|---|
| root | `version`(반드시 1), `sessionId`, `nextBlockId`, `blocks[]`, `messageRefs`, optional `tokenSnapshot`, `nudge`, `stats`, optional `derivedFrom` |
| `blocks[]` | `blockId`, `tier`, optional `topic`, `summary`, optional `directMessageIds[]`, `effectiveMessageIds[]`, `directBlockIds[]`, `archivePath`, `archiveRel`, `compressedTokens`, `startRef`, `endRef`, `createdAt`, `active`, `compressCallId` |
| `messageRefs` | `byRaw` raw-id→ref map, `byRef` ref→raw-id map |
| `nudge` | `lastPerMessageNudgeTokens`, `lastNudgeShownTokens`, optional `lastShownByTier` string-tier→token map |
| `stats` | `tokensCompressed`, `compressionCount` |

missing file은 initial state다. malformed JSON, I/O error, 또는 `version != 1`은 `Load` error와 initial state를 반환한다. `noaadapter.newSession`은 warning 후 **empty state로 계속하며 자동 rebuild하지 않는다**. 명시적 `RebuildFromHistory`만 transcript의 recorded Compress calls를 replay하고 기존 archive를 재사용해 state를 저장한다. inherited state search는 최대 8 ancestor를 보고 corrupt/empty ancestor를 건너뛴다. [state 근거](evidence:noa-state-schema) [rebuild 근거](evidence:noa-rebuild)

<a id="noa-archive"></a>

## Noa archive Markdown

path는 `<root>/tier<N>/<blockID>_<startRef>-<endRef>.md`다. 파일 단위 temp+rename이며 archive와 `state.json`은 하나의 transaction이 아니다.

| section | format |
|---|---|
| frontmatter required | `block`, `tier`, `range`, `original_tokens` |
| frontmatter optional | `session`, `created`, `topic`; tier 1은 `message_count`, upper tier는 `consumed_blocks: [...]`와 `loose_messages` |
| heading | archive id/tier/ref span |
| active summary | blockquote로 표시 |
| raw entries | text/thinking 원문, tool-use JSON과 tool-result를 collision-safe Markdown fence로 **truncate 없이** 기록 |
| absorbed block | child summary, relative archive link, absolute path |

archive reader schema/parser는 없다. Materialize는 state를 이용해 messages를 prune할 뿐 archive 파일을 다시 읽지 않으며, missing/corrupt archive를 자동 감지하지 않는다. replay archiver는 같은 expected path가 존재하면 내용 검증 없이 채택한다. 따라서 archive 무결성·future migration·dangling state pointer는 host audit 대상이다. [archive format 근거](evidence:noa-archive) [file writer 근거](evidence:noa-archive-writer) [materialize 근거](evidence:noa-rebuild)

<a id="consumers"></a>

## Writer와 consumer 연결

| artifact | writer | primary consumers |
|---|---|---|
| transcript main | `agentcore.Session.Prompt` recorder | `Session.Resume`, offline audit, Noa `RebuildFromHistory` input |
| transcript sidechain | `subagent.run` recorder | offline child accounting; parent totals에 자동 합산 안 됨 |
| memory file/index | `RecordMemory`/`Store.Save` | `RecallMemory`, auto-inject, system index |
| `state.json` | Noa adapter after state changes/rebuild | `Session.View`, Materialize, inheritance |
| archive Markdown | `ApplyCompression` through file archiver | human/Read-tool recovery path; state stores pointer |

[Session consumer 근거](evidence:agentcore-resume) [Noa consumer 근거](evidence:noa-projection)
