Skip to content

Commit 4b8de57

Browse files
committed
feat: persist governed semantic turn artifacts
1 parent befa85a commit 4b8de57

24 files changed

Lines changed: 1933 additions & 418 deletions

core/outer_brain_journal/lib/outer_brain/journal/tables.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ defmodule OuterBrain.Journal.Tables do
246246
:dedupe_key,
247247
:body,
248248
:body_ref,
249+
:run_ref,
250+
:turn_ref,
251+
:attempt_ref,
252+
:reply_artifact_ref,
253+
:next_semantic_ref,
249254
persistence_posture: PersistencePosture.memory(:publication_state)
250255
]
251256

@@ -257,6 +262,11 @@ defmodule OuterBrain.Journal.Tables do
257262
dedupe_key: String.t(),
258263
body: String.t(),
259264
body_ref: ReplyBodyBoundary.body_ref(),
265+
run_ref: String.t() | nil,
266+
turn_ref: String.t() | nil,
267+
attempt_ref: String.t() | nil,
268+
reply_artifact_ref: String.t() | nil,
269+
next_semantic_ref: String.t() | nil,
260270
persistence_posture: PersistencePosture.t()
261271
}
262272

@@ -286,6 +296,11 @@ defmodule OuterBrain.Journal.Tables do
286296
dedupe_key: dedupe_key,
287297
body: body,
288298
body_ref: body_ref,
299+
run_ref: Map.get(attrs, :run_ref),
300+
turn_ref: Map.get(attrs, :turn_ref),
301+
attempt_ref: Map.get(attrs, :attempt_ref),
302+
reply_artifact_ref: Map.get(attrs, :reply_artifact_ref),
303+
next_semantic_ref: Map.get(attrs, :next_semantic_ref),
289304
persistence_posture: PersistencePosture.resolve(:publication_state, attrs)
290305
}}
291306
else

core/outer_brain_persistence/README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,24 @@ Canonical durable tables include:
99
- `recovery_tasks`
1010
- `reply_publications`
1111
- `outer_brain_artifact_descriptors`
12+
- `outer_brain_artifact_payloads`
1213
- `outer_brain_semantic_contexts`
1314

1415
This package owns the canonical write path for those rows. In-memory runtime
1516
state may mirror hot rows, but it does not own truth.
1617

1718
Semantic failure carriers are recorded as idempotent
1819
`semantic_journal_entries` with `entry_type = "semantic_failure"` and payloads
19-
encoded through `OuterBrain.Contracts.SemanticFailure`. Reply publication
20-
writes are idempotent by `dedupe_key`, so restart replay can update the durable
21-
publication row without creating a second user-visible publication.
22-
23-
Semantic-context writes atomically persist a secret-free
24-
`GroundPlane.Contracts.ArtifactDescriptor` and immutable
25-
`OuterBrain.Contracts.SemanticContextProvenance`. The PostgreSQL full-text
26-
index covers opaque semantic, provider, model, artifact, and provenance refs;
27-
it never indexes raw prompt or provider bodies.
20+
encoded through `OuterBrain.Contracts.SemanticFailure`. Final reply publication
21+
writes are immutable and idempotent by exact `dedupe_key`/lineage agreement, so
22+
restart replay cannot create or mutate a second user-visible publication.
23+
24+
Prompt-context and reply-continuation writes atomically persist immutable
25+
content-addressed payloads, secret-free
26+
`GroundPlane.Contracts.ArtifactDescriptor` rows, semantic provenance, and exact
27+
run/turn/attempt lineage. Payload reads require tenant, reader, operation, and
28+
authority-packet agreement. The PostgreSQL full-text index covers only opaque
29+
refs; it never indexes prompt or provider bodies.
2830

2931
Production hosts supervise
3032
`{OuterBrain.Persistence.DurableSupervisor, profile: :durable_redacted,

core/outer_brain_persistence/docs/persistence.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
## Production contract
44

55
`core/outer_brain_persistence` owns PostgreSQL truth for semantic-session
6-
leases, journal entries, recovery tasks, reply publications, artifact
7-
descriptors, and semantic-context provenance. The sole production profile is
6+
leases, journal entries, recovery tasks, reply publications, immutable artifact
7+
descriptors/payloads, and semantic-context provenance. The sole production profile is
88
`:durable_redacted`; missing, disabled, memory, and unknown profiles fail before
99
a repository child is selected.
1010

@@ -44,16 +44,22 @@ OuterBrain.Persistence.Store.preflight(
4444

4545
## Semantic context and artifact boundaries
4646

47-
`Store.record_semantic_context/3` atomically records an immutable,
48-
secret-free `GroundPlane.Contracts.ArtifactDescriptor` and the matching
49-
`OuterBrain.Contracts.SemanticContextProvenance`. Exact replays are idempotent;
50-
reuse of an artifact, semantic, or idempotency reference with different facts
51-
fails closed. Tenant scope is required on every write and read.
47+
`Store.record_prompt_context/2` atomically records the content-addressed context
48+
and prompt-manifest payloads, their secret-free descriptors, and matching
49+
semantic provenance/lineage. `Store.publish_reply_continuation/2` atomically
50+
records the normalized final reply, next context revision, publication, and a
51+
safe journal fact. Exact replays are idempotent; reuse of an artifact, semantic,
52+
publication, or idempotency reference with different facts fails closed. Tenant
53+
scope is required on every write and read.
54+
55+
`Store.resolve_artifact_payload/3` requires exact tenant, reader, operation,
56+
and authority-packet agreement. Artifact references are not bearer authority.
5257

5358
The semantic index contains only opaque semantic, provider, model, artifact,
54-
and provenance refs. Raw prompts, provider bodies, signed object-store URLs,
55-
credentials, and credential-shaped metadata are forbidden. Object locations
56-
remain opaque owner-authorized refs.
59+
run, turn, and provenance refs. Provider-native bodies, private reasoning,
60+
credentials, signed object-store URLs, and credential-shaped metadata are
61+
forbidden. Payloads are immutable and accessible only through the owner API;
62+
locations remain opaque owner-authorized refs.
5763

5864
## Test boundary
5965

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
defmodule OuterBrain.Persistence.ArtifactAccess do
2+
@moduledoc """
3+
Safe, exact authorization evidence required to resolve an OuterBrain artifact.
4+
5+
An artifact ref is never bearer authority. The tenant, reader, operation, and
6+
authority packet must all agree with the immutable payload row.
7+
"""
8+
9+
@enforce_keys [:tenant_ref, :reader_ref, :operation_ref, :authority_packet_ref]
10+
defstruct @enforce_keys
11+
12+
@type t :: %__MODULE__{
13+
tenant_ref: String.t(),
14+
reader_ref: String.t(),
15+
operation_ref: String.t(),
16+
authority_packet_ref: String.t()
17+
}
18+
19+
@spec new(map() | keyword() | t()) :: {:ok, t()} | {:error, :invalid_artifact_access}
20+
def new(%__MODULE__{} = access), do: access |> Map.from_struct() |> new()
21+
22+
def new(attrs) when is_map(attrs) or is_list(attrs) do
23+
attrs = Map.new(attrs)
24+
25+
with {:ok, tenant_ref} <- required(attrs, :tenant_ref),
26+
{:ok, reader_ref} <- required(attrs, :reader_ref),
27+
{:ok, operation_ref} <- required(attrs, :operation_ref),
28+
{:ok, authority_packet_ref} <- required(attrs, :authority_packet_ref) do
29+
{:ok,
30+
%__MODULE__{
31+
tenant_ref: tenant_ref,
32+
reader_ref: reader_ref,
33+
operation_ref: operation_ref,
34+
authority_packet_ref: authority_packet_ref
35+
}}
36+
else
37+
_other -> {:error, :invalid_artifact_access}
38+
end
39+
end
40+
41+
def new(_attrs), do: {:error, :invalid_artifact_access}
42+
43+
@spec new!(map() | keyword() | t()) :: t()
44+
def new!(attrs) do
45+
case new(attrs) do
46+
{:ok, access} -> access
47+
{:error, reason} -> raise ArgumentError, "invalid artifact access: #{inspect(reason)}"
48+
end
49+
end
50+
51+
defp required(attrs, key) do
52+
case Map.get(attrs, key, Map.get(attrs, Atom.to_string(key))) do
53+
value when is_binary(value) and value != "" -> {:ok, value}
54+
_other -> :error
55+
end
56+
end
57+
end

core/outer_brain_persistence/lib/outer_brain/persistence/mappers/reply_publication_mapper.ex

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ defmodule OuterBrain.Persistence.ReplyPublicationMapper do
33

44
alias OuterBrain.Journal.Tables.ReplyPublicationRecord
55

6-
@spec to_schema_attrs(String.t(), ReplyPublicationRecord.t()) :: map()
7-
def to_schema_attrs(tenant_id, %ReplyPublicationRecord{} = publication) do
6+
@spec to_schema_attrs(String.t(), ReplyPublicationRecord.t(), map()) :: map()
7+
def to_schema_attrs(tenant_id, %ReplyPublicationRecord{} = publication, lineage) do
88
%{
99
publication_id: publication.publication_id,
1010
tenant_id: tenant_id,
@@ -13,7 +13,12 @@ defmodule OuterBrain.Persistence.ReplyPublicationMapper do
1313
state: publication.state,
1414
dedupe_key: publication.dedupe_key,
1515
body: publication.body,
16-
body_ref: publication.body_ref
16+
body_ref: publication.body_ref,
17+
run_ref: lineage.run_ref,
18+
turn_ref: lineage.turn_ref,
19+
attempt_ref: lineage.attempt_ref,
20+
reply_artifact_ref: lineage.reply_artifact_ref,
21+
next_semantic_ref: lineage.next_semantic_ref
1722
}
1823
end
1924

@@ -27,7 +32,12 @@ defmodule OuterBrain.Persistence.ReplyPublicationMapper do
2732
state: schema.state,
2833
dedupe_key: schema.dedupe_key,
2934
body: schema.body,
30-
body_ref: schema.body_ref
35+
body_ref: schema.body_ref,
36+
run_ref: schema.run_ref,
37+
turn_ref: schema.turn_ref,
38+
attempt_ref: schema.attempt_ref,
39+
reply_artifact_ref: schema.reply_artifact_ref,
40+
next_semantic_ref: schema.next_semantic_ref
3141
})
3242

3343
publication

core/outer_brain_persistence/lib/outer_brain/persistence/mappers/semantic_context_mapper.ex

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ defmodule OuterBrain.Persistence.SemanticContextMapper do
44
alias GroundPlane.Boundary.Codec
55
alias OuterBrain.Contracts.SemanticContextProvenance
66

7-
@spec to_schema_attrs(SemanticContextProvenance.t(), String.t()) :: map()
8-
def to_schema_attrs(%SemanticContextProvenance{} = provenance, artifact_ref) do
9-
attrs = SemanticContextProvenance.to_map(provenance)
7+
@spec to_schema_attrs(SemanticContextProvenance.t(), map()) :: map()
8+
def to_schema_attrs(%SemanticContextProvenance{} = provenance, lineage) do
9+
provenance_attrs = SemanticContextProvenance.to_map(provenance)
10+
attrs = Map.merge(provenance_attrs, lineage)
1011

1112
attrs
12-
|> Map.put(:artifact_ref, artifact_ref)
13-
|> Map.put(:provenance_digest, Codec.digest(attrs))
14-
|> Map.put(:search_document, search_document(provenance, artifact_ref))
13+
|> Map.put(
14+
:provenance_digest,
15+
Codec.digest(%{provenance: provenance_attrs, lineage: lineage})
16+
)
17+
|> Map.put(:search_document, search_document(provenance, lineage))
1518
end
1619

1720
@spec from_schema(struct()) :: SemanticContextProvenance.t()
@@ -47,12 +50,28 @@ defmodule OuterBrain.Persistence.SemanticContextMapper do
4750
provenance
4851
end
4952

50-
defp search_document(provenance, artifact_ref) do
53+
@spec lineage_from_schema(struct()) :: map()
54+
def lineage_from_schema(schema) do
55+
%{
56+
run_ref: schema.run_ref,
57+
turn_ref: schema.turn_ref,
58+
context_artifact_ref: schema.context_artifact_ref,
59+
prompt_artifact_ref: schema.prompt_artifact_ref,
60+
model_profile_ref: schema.model_profile_ref,
61+
memory_snapshot_refs: schema.memory_snapshot_refs,
62+
previous_semantic_ref: schema.previous_semantic_ref
63+
}
64+
end
65+
66+
defp search_document(provenance, lineage) do
5167
[
5268
provenance.semantic_ref,
5369
provenance.provider_ref,
5470
provenance.model_ref,
55-
artifact_ref
71+
lineage.context_artifact_ref,
72+
lineage.prompt_artifact_ref,
73+
lineage.run_ref,
74+
lineage.turn_ref
5675
| provenance.provenance_refs
5776
]
5877
|> Enum.join(" ")

0 commit comments

Comments
 (0)