Step 5: close voice and forge schema gaps.

7e3d7fc80e12 · AtlantisPleb · · parent 1847a1102b5f

Step 5: close voice and forge schema gaps.

- Adds the two missing voice migrations and the raw-arguments follow-up so voice_tool_steps and voice_response_contexts exist.

- Replaces OpenAgents.Forge.PushReceipt with the real Sarah schema and recreates create_forge_deploys to match OpenAgents.Forge.DeployReceipt.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added priv/repo/migrations/20260816224054_govern_voice_context_tools_and_chronology.exs
  • added priv/repo/migrations/20260818003949_add_raw_arguments_to_voice_tool_steps.exs
  • deleted priv/repo/migrations/20260820021249_create_voice_response_contexts.exs
  • deleted priv/repo/migrations/20260820021249_create_voice_tool_steps.exs

Diff

4 files changed, +490 -14

priv/repo/migrations/20260816224054_govern_voice_context_tools_and_chronology.exs added +400

@@ -0,0 +1,400 @@

1
defmodule OpenAgents.Repo.Migrations.GovernVoiceContextToolsAndChronology do
2
  use Ecto.Migration
3
4
  def up do
5
    create constraint(:messages, :messages_modality_check, check: "modality IN ('text', 'voice')")
6
7
    create constraint(:messages, :messages_voice_provenance_check,
8
             check:
9
               "(modality = 'text' AND voice_session_id IS NULL AND provider_item_id IS NULL AND transcript_kind IS NULL AND interrupted = false) OR " <>
10
                 "(modality = 'voice' AND voice_session_id IS NOT NULL AND provider_item_id IS NOT NULL AND transcript_kind IN ('provider_input_transcription', 'provider_output_transcript'))"
11
           )
12
13
    execute("""
14
    CREATE FUNCTION enforce_voice_message_transition() RETURNS trigger AS $$
15
    BEGIN
16
      IF OLD.modality = 'voice' THEN
17
        IF ROW(
18
          OLD.conversation_id, OLD.role, OLD.content, OLD.modality,
19
          OLD.voice_session_id, OLD.provider_item_id, OLD.transcript_kind
20
        ) IS DISTINCT FROM ROW(
21
          NEW.conversation_id, NEW.role, NEW.content, NEW.modality,
22
          NEW.voice_session_id, NEW.provider_item_id, NEW.transcript_kind
23
        ) THEN
24
          RAISE EXCEPTION 'voice message evidence is immutable';
25
        END IF;
26
27
        IF OLD.status IN ('complete', 'failed', 'cancelled') AND
28
           ROW(OLD.status, OLD.interrupted) IS DISTINCT FROM
29
           ROW(NEW.status, NEW.interrupted) THEN
30
          RAISE EXCEPTION 'terminal voice message is immutable';
31
        END IF;
32
33
        IF OLD.interrupted = true AND NEW.interrupted = false THEN
34
          RAISE EXCEPTION 'voice interruption is irreversible';
35
        END IF;
36
      END IF;
37
      RETURN NEW;
38
    END;
39
    $$ LANGUAGE plpgsql;
40
    """)
41
42
    execute("""
43
    CREATE TRIGGER messages_enforce_voice_transition
44
    BEFORE UPDATE ON messages
45
    FOR EACH ROW EXECUTE FUNCTION enforce_voice_message_transition();
46
    """)
47
48
    alter table(:voice_sessions) do
49
      add :instructions, :text, null: false, default: ""
50
51
      add :tool_catalog, :map,
52
        null: false,
53
        default: %{"schema" => "sarah.realtime_tool_catalog.v1", "tools" => []}
54
55
      add :blueprint_revision, :string
56
      add :blueprint_digest, :string
57
      add :program_artifact_id, :string
58
      add :program_artifact_digest, :string
59
      add :program_artifact_receipt, :map
60
    end
61
62
    create constraint(:voice_sessions, :voice_sessions_context_digest_check,
63
             check:
64
               "(blueprint_digest IS NULL OR blueprint_digest ~ '^[0-9a-f]{64}$') AND " <>
65
                 "(program_artifact_digest IS NULL OR program_artifact_digest ~ '^[0-9a-f]{64}$') AND " <>
66
                 "octet_length(instructions) <= 65536 AND octet_length(tool_catalog::text) <= 65536"
67
           )
68
69
    create constraint(:voice_sessions, :voice_sessions_tool_catalog_check,
70
             check:
71
               "jsonb_typeof(tool_catalog) = 'object' AND " <>
72
                 "tool_catalog->>'schema' = 'sarah.realtime_tool_catalog.v1' AND " <>
73
                 "jsonb_typeof(tool_catalog->'tools') = 'array'"
74
           )
75
76
    create constraint(:voice_sessions, :voice_sessions_program_capture_check,
77
             check:
78
               "program_artifact_receipt IS NULL OR (" <>
79
                 "jsonb_typeof(program_artifact_receipt) = 'object' AND " <>
80
                 "program_artifact_receipt->>'schema' = 'sarah.program_capture.v1' AND " <>
81
                 "program_artifact_receipt->>'artifact_id' IS NOT DISTINCT FROM program_artifact_id AND " <>
82
                 "program_artifact_receipt->>'artifact_digest' IS NOT DISTINCT FROM program_artifact_digest AND " <>
83
                 "jsonb_typeof(program_artifact_receipt->'degraded') = 'boolean' AND " <>
84
                 "octet_length(program_artifact_receipt::text) <= 4096)"
85
           )
86
87
    create table(:voice_response_contexts, primary_key: false) do
88
      add :id, :binary_id, primary_key: true
89
90
      add :voice_session_id,
91
          references(:voice_sessions, type: :binary_id, on_delete: :delete_all),
92
          null: false
93
94
      add :generation, :integer, null: false
95
96
      add :user_message_id,
97
          references(:messages, type: :binary_id, on_delete: :restrict),
98
          null: false
99
100
      add :provider_input_item_id, :string, null: false
101
      add :instructions, :text, null: false
102
      add :instruction_digest, :string, null: false
103
      add :memory_snapshot_ref, :string, null: false
104
      add :profile_memory_snapshot_ref, :string, null: false
105
      add :selected_evidence, :map, null: false
106
      add :selected_source_refs, {:array, :string}, null: false, default: []
107
      add :program_artifact_id, :string
108
      add :program_artifact_digest, :string
109
      add :program_artifact_receipt, :map, null: false
110
      add :captured_at, :utc_datetime_usec, null: false
111
      timestamps(type: :utc_datetime_usec, updated_at: false)
112
    end
113
114
    create unique_index(
115
             :voice_response_contexts,
116
             [:voice_session_id, :generation, :provider_input_item_id],
117
             name: :voice_response_context_input_item_index
118
           )
119
120
    create unique_index(:voice_response_contexts, [:user_message_id])
121
122
    create constraint(:voice_response_contexts, :voice_response_contexts_digest_check,
123
             check:
124
               "instruction_digest ~ '^[0-9a-f]{64}$' AND " <>
125
                 "(program_artifact_digest IS NULL OR program_artifact_digest ~ '^[0-9a-f]{64}$')"
126
           )
127
128
    create constraint(:voice_response_contexts, :voice_response_contexts_snapshot_check,
129
             check:
130
               "memory_snapshot_ref ~ '^message:[0-9a-f-]{36}$' AND " <>
131
                 "profile_memory_snapshot_ref ~ '^profile-memory-snapshot:v1:[0-9a-f-]{36}$'"
132
           )
133
134
    create constraint(:voice_response_contexts, :voice_response_contexts_payload_check,
135
             check:
136
               "octet_length(instructions) BETWEEN 1 AND 65536 AND " <>
137
                 "octet_length(selected_evidence::text) <= 65536 AND " <>
138
                 "octet_length(program_artifact_receipt::text) <= 4096"
139
           )
140
141
    alter table(:voice_response_receipts) do
142
      add :response_context_id,
143
          references(:voice_response_contexts, type: :binary_id, on_delete: :restrict)
144
145
      add :assistant_message_id, references(:messages, type: :binary_id, on_delete: :restrict)
146
      add :used_source_refs, {:array, :string}, null: false, default: []
147
      add :used_tool_step_refs, {:array, :string}, null: false, default: []
148
149
      add :used_memory_evidence, :map,
150
        null: false,
151
        default: %{"schema" => "sarah.memory_evidence_usage.v1", "items" => []}
152
    end
153
154
    alter table(:voice_transcript_items) do
155
      add :message_id, references(:messages, type: :binary_id, on_delete: :restrict)
156
    end
157
158
    create unique_index(:voice_transcript_items, [:message_id], where: "message_id IS NOT NULL")
159
160
    create table(:voice_tool_steps, primary_key: false) do
161
      add :id, :binary_id, primary_key: true
162
163
      add :voice_session_id,
164
          references(:voice_sessions, type: :binary_id, on_delete: :delete_all),
165
          null: false
166
167
      add :voice_response_receipt_id,
168
          references(:voice_response_receipts, type: :binary_id, on_delete: :delete_all),
169
          null: false
170
171
      add :generation, :integer, null: false
172
      add :sequence, :integer, null: false
173
      add :provider_call_id, :string, null: false
174
      add :provider_item_id, :string, null: false
175
      add :provider_response_id, :string, null: false
176
      add :tool_name, :string, null: false
177
      add :tool_version, :integer, null: false
178
      add :module_id, :string, null: false
179
      add :catalog_digest, :string, null: false
180
      add :argument_digest, :string, null: false
181
      add :status, :string, null: false, default: "requested"
182
      add :outcome_digest, :string
183
      add :result, :map
184
      add :error, :map
185
      add :executor_id, :string
186
      add :executor_disclosure, :string
187
      add :target_receipt_refs, {:array, :string}, null: false, default: []
188
      add :attribution_refs, {:array, :string}, null: false, default: []
189
      add :requested_at, :utc_datetime_usec, null: false
190
      add :started_at, :utc_datetime_usec
191
      add :completed_at, :utc_datetime_usec
192
      timestamps(type: :utc_datetime_usec)
193
    end
194
195
    create unique_index(:voice_tool_steps, [:voice_session_id, :generation, :sequence],
196
             name: :voice_tool_steps_sequence_index
197
           )
198
199
    create unique_index(:voice_tool_steps, [:voice_session_id, :generation, :provider_call_id],
200
             name: :voice_tool_steps_provider_call_index
201
           )
202
203
    create constraint(:voice_tool_steps, :voice_tool_steps_sequence_check,
204
             check: "sequence > 0 AND sequence <= 32"
205
           )
206
207
    create constraint(:voice_tool_steps, :voice_tool_steps_status_check,
208
             check:
209
               "status IN ('requested', 'running', 'succeeded', 'failed', 'refused', 'cancelled', 'unavailable', 'interrupted')"
210
           )
211
212
    create constraint(:voice_tool_steps, :voice_tool_steps_digest_check,
213
             check:
214
               "catalog_digest ~ '^[0-9a-f]{64}$' AND argument_digest ~ '^[0-9a-f]{64}$' AND (outcome_digest IS NULL OR outcome_digest ~ '^[0-9a-f]{64}$')"
215
           )
216
217
    create constraint(:voice_tool_steps, :voice_tool_steps_lifecycle_shape_check,
218
             check:
219
               "(status = 'requested' AND started_at IS NULL AND completed_at IS NULL AND outcome_digest IS NULL AND result IS NULL AND error IS NULL) OR " <>
220
                 "(status = 'running' AND started_at IS NOT NULL AND completed_at IS NULL AND outcome_digest IS NULL AND result IS NULL AND error IS NULL) OR " <>
221
                 "(status = 'succeeded' AND completed_at IS NOT NULL AND outcome_digest IS NOT NULL AND result IS NOT NULL AND error IS NULL AND executor_id IS NOT NULL AND executor_disclosure IS NOT NULL) OR " <>
222
                 "(status IN ('failed', 'refused', 'cancelled', 'unavailable', 'interrupted') AND completed_at IS NOT NULL AND outcome_digest IS NOT NULL AND result IS NULL AND error IS NOT NULL AND executor_id IS NOT NULL AND executor_disclosure IS NOT NULL)"
223
           )
224
225
    execute("""
226
    CREATE OR REPLACE FUNCTION enforce_voice_session_identity_immutable() RETURNS trigger AS $$
227
    BEGIN
228
      IF NEW.conversation_id IS DISTINCT FROM OLD.conversation_id
229
         OR NEW.generation IS DISTINCT FROM OLD.generation
230
         OR NEW.architecture IS DISTINCT FROM OLD.architecture
231
         OR NEW.provider_id IS DISTINCT FROM OLD.provider_id
232
         OR NEW.model_id IS DISTINCT FROM OLD.model_id
233
         OR NEW.voice_artifact_id IS DISTINCT FROM OLD.voice_artifact_id
234
         OR NEW.persona_id IS DISTINCT FROM OLD.persona_id
235
         OR NEW.persona_digest IS DISTINCT FROM OLD.persona_digest
236
         OR NEW.role_id IS DISTINCT FROM OLD.role_id
237
         OR NEW.role_digest IS DISTINCT FROM OLD.role_digest
238
         OR NEW.role_selection IS DISTINCT FROM OLD.role_selection
239
         OR NEW.instruction_digest IS DISTINCT FROM OLD.instruction_digest
240
         OR NEW.instructions IS DISTINCT FROM OLD.instructions
241
         OR NEW.tool_catalog_digest IS DISTINCT FROM OLD.tool_catalog_digest
242
         OR NEW.tool_catalog IS DISTINCT FROM OLD.tool_catalog
243
         OR NEW.blueprint_revision IS DISTINCT FROM OLD.blueprint_revision
244
         OR NEW.blueprint_digest IS DISTINCT FROM OLD.blueprint_digest
245
         OR NEW.program_artifact_id IS DISTINCT FROM OLD.program_artifact_id
246
         OR NEW.program_artifact_digest IS DISTINCT FROM OLD.program_artifact_digest
247
         OR NEW.program_artifact_receipt IS DISTINCT FROM OLD.program_artifact_receipt
248
         OR (OLD.provider_session_id IS NOT NULL AND
249
             NEW.provider_session_id IS DISTINCT FROM OLD.provider_session_id)
250
         OR NEW.started_at IS DISTINCT FROM OLD.started_at THEN
251
        RAISE EXCEPTION 'voice session identity is immutable';
252
      END IF;
253
      RETURN NEW;
254
    END;
255
    $$ LANGUAGE plpgsql;
256
    """)
257
258
    execute("""
259
    CREATE TRIGGER voice_response_context_generation_matches
260
    BEFORE INSERT OR UPDATE ON voice_response_contexts
261
    FOR EACH ROW EXECUTE FUNCTION enforce_voice_child_generation();
262
    """)
263
264
    execute("""
265
    CREATE TRIGGER voice_tool_step_generation_matches
266
    BEFORE INSERT OR UPDATE ON voice_tool_steps
267
    FOR EACH ROW EXECUTE FUNCTION enforce_voice_child_generation();
268
    """)
269
270
    execute("""
271
    CREATE FUNCTION enforce_voice_tool_step_transition()
272
    RETURNS trigger AS $$
273
    BEGIN
274
      IF ROW(
275
        OLD.voice_session_id, OLD.voice_response_receipt_id, OLD.generation,
276
        OLD.sequence, OLD.provider_call_id, OLD.provider_item_id,
277
        OLD.provider_response_id, OLD.tool_name, OLD.tool_version,
278
        OLD.module_id, OLD.catalog_digest, OLD.argument_digest, OLD.requested_at
279
      ) IS DISTINCT FROM ROW(
280
        NEW.voice_session_id, NEW.voice_response_receipt_id, NEW.generation,
281
        NEW.sequence, NEW.provider_call_id, NEW.provider_item_id,
282
        NEW.provider_response_id, NEW.tool_name, NEW.tool_version,
283
        NEW.module_id, NEW.catalog_digest, NEW.argument_digest, NEW.requested_at
284
      ) THEN
285
        RAISE EXCEPTION 'voice tool step identity is immutable';
286
      END IF;
287
288
      IF OLD.status = 'requested' AND NEW.status NOT IN (
289
        'requested', 'running', 'succeeded', 'failed', 'refused',
290
        'cancelled', 'unavailable', 'interrupted'
291
      ) THEN
292
        RAISE EXCEPTION 'invalid requested voice tool step transition';
293
      END IF;
294
295
      IF OLD.status = 'running' AND NEW.status NOT IN (
296
        'running', 'succeeded', 'failed', 'refused', 'cancelled',
297
        'unavailable', 'interrupted'
298
      ) THEN
299
        RAISE EXCEPTION 'invalid running voice tool step transition';
300
      END IF;
301
302
      IF OLD.status NOT IN ('requested', 'running') AND ROW(
303
        OLD.status, OLD.outcome_digest, OLD.result, OLD.error,
304
        OLD.executor_id, OLD.executor_disclosure, OLD.target_receipt_refs,
305
        OLD.attribution_refs, OLD.started_at, OLD.completed_at
306
      ) IS DISTINCT FROM ROW(
307
        NEW.status, NEW.outcome_digest, NEW.result, NEW.error,
308
        NEW.executor_id, NEW.executor_disclosure, OLD.target_receipt_refs,
309
        NEW.attribution_refs, NEW.started_at, NEW.completed_at
310
      ) THEN
311
        RAISE EXCEPTION 'terminal voice tool step is immutable';
312
      END IF;
313
314
      RETURN NEW;
315
    END;
316
    $$ LANGUAGE plpgsql;
317
    """)
318
319
    execute("""
320
    CREATE TRIGGER voice_tool_steps_enforce_transition
321
    BEFORE UPDATE ON voice_tool_steps
322
    FOR EACH ROW EXECUTE FUNCTION enforce_voice_tool_step_transition();
323
    """)
324
  end
325
326
  def down do
327
    execute("DROP TRIGGER IF EXISTS voice_tool_steps_enforce_transition ON voice_tool_steps")
328
    execute("DROP FUNCTION IF EXISTS enforce_voice_tool_step_transition()")
329
    execute("DROP TRIGGER IF EXISTS voice_tool_step_generation_matches ON voice_tool_steps")
330
331
    execute(
332
      "DROP TRIGGER IF EXISTS voice_response_context_generation_matches ON voice_response_contexts"
333
    )
334
335
    execute("DROP TRIGGER IF EXISTS messages_enforce_voice_transition ON messages")
336
    execute("DROP FUNCTION IF EXISTS enforce_voice_message_transition()")
337
338
    drop table(:voice_tool_steps)
339
340
    alter table(:voice_transcript_items) do
341
      remove :message_id
342
    end
343
344
    alter table(:voice_response_receipts) do
345
      remove :used_memory_evidence
346
      remove :used_tool_step_refs
347
      remove :used_source_refs
348
      remove :assistant_message_id
349
      remove :response_context_id
350
    end
351
352
    drop table(:voice_response_contexts)
353
354
    alter table(:voice_sessions) do
355
      remove :program_artifact_receipt
356
      remove :program_artifact_digest
357
      remove :program_artifact_id
358
      remove :blueprint_digest
359
      remove :blueprint_revision
360
      remove :tool_catalog
361
      remove :instructions
362
    end
363
364
    execute("""
365
    CREATE OR REPLACE FUNCTION enforce_voice_session_identity_immutable() RETURNS trigger AS $$
366
    BEGIN
367
      IF NEW.conversation_id IS DISTINCT FROM OLD.conversation_id
368
         OR NEW.generation IS DISTINCT FROM OLD.generation
369
         OR NEW.architecture IS DISTINCT FROM OLD.architecture
370
         OR NEW.provider_id IS DISTINCT FROM OLD.provider_id
371
         OR NEW.model_id IS DISTINCT FROM OLD.model_id
372
         OR NEW.voice_artifact_id IS DISTINCT FROM OLD.voice_artifact_id
373
         OR NEW.persona_id IS DISTINCT FROM OLD.persona_id
374
         OR NEW.persona_digest IS DISTINCT FROM OLD.persona_digest
375
         OR NEW.role_id IS DISTINCT FROM OLD.role_id
376
         OR NEW.role_digest IS DISTINCT FROM OLD.role_digest
377
         OR NEW.role_selection IS DISTINCT FROM OLD.role_selection
378
         OR NEW.instruction_digest IS DISTINCT FROM OLD.instruction_digest
379
         OR NEW.instructions IS DISTINCT FROM OLD.instructions
380
         OR NEW.tool_catalog_digest IS DISTINCT FROM OLD.tool_catalog_digest
381
         OR NEW.tool_catalog IS DISTINCT FROM OLD.tool_catalog
382
         OR NEW.blueprint_revision IS DISTINCT FROM OLD.blueprint_revision
383
         OR NEW.blueprint_digest IS DISTINCT FROM OLD.blueprint_digest
384
         OR NEW.program_artifact_id IS DISTINCT FROM OLD.program_artifact_id
385
         OR NEW.program_artifact_digest IS DISTINCT FROM OLD.program_artifact_digest
386
         OR NEW.program_artifact_receipt IS DISTINCT FROM OLD.program_artifact_receipt
387
         OR (OLD.provider_session_id IS NOT NULL AND
388
             NEW.provider_session_id IS DISTINCT FROM OLD.provider_session_id)
389
         OR NEW.started_at IS DISTINCT FROM OLD.started_at THEN
390
        RAISE EXCEPTION 'voice session identity is immutable';
391
      END IF;
392
      RETURN NEW;
393
    END;
394
    $$ LANGUAGE plpgsql;
395
    """)
396
397
    drop constraint(:messages, :messages_voice_provenance_check)
398
    drop constraint(:messages, :messages_modality_check)
399
  end
400
end
priv/repo/migrations/20260818003949_add_raw_arguments_to_voice_tool_steps.exs added +90

@@ -0,0 +1,90 @@

1
defmodule OpenAgents.Repo.Migrations.AddRawArgumentsToVoiceToolSteps do
2
  use Ecto.Migration
3
4
  # Owner-directed policy change (issue #72): the durable voice tool step now
5
  # retains the raw Realtime function arguments as user-owned, deletable
6
  # conversation evidence alongside the canonical argument digest. The durable
7
  # voice event ledger stays digest-only; this row is the raw arguments' home.
8
  # The column is nullable because historical rows predate the policy. The
9
  # byte ceiling matches the existing `validate_raw_tool_arguments/1` bound in
10
  # `Sarah.Voice`, and the transition trigger freezes the value with the rest
11
  # of the request identity.
12
13
  def up do
14
    alter table(:voice_tool_steps) do
15
      add :raw_arguments, :text
16
    end
17
18
    create constraint(:voice_tool_steps, :voice_tool_steps_raw_arguments_bound_check,
19
             check: "raw_arguments IS NULL OR octet_length(raw_arguments) <= 16384"
20
           )
21
22
    replace_transition_function(true)
23
  end
24
25
  def down do
26
    replace_transition_function(false)
27
28
    drop constraint(:voice_tool_steps, :voice_tool_steps_raw_arguments_bound_check)
29
30
    alter table(:voice_tool_steps) do
31
      remove :raw_arguments
32
    end
33
  end
34
35
  defp replace_transition_function(raw_arguments_identity?) do
36
    old_raw = if raw_arguments_identity?, do: "OLD.raw_arguments,", else: ""
37
    new_raw = if raw_arguments_identity?, do: "NEW.raw_arguments,", else: ""
38
39
    execute("""
40
    CREATE OR REPLACE FUNCTION enforce_voice_tool_step_transition()
41
    RETURNS trigger AS $$
42
    BEGIN
43
      IF ROW(
44
        OLD.voice_session_id, OLD.voice_response_receipt_id, OLD.generation,
45
        OLD.sequence, OLD.provider_call_id, OLD.provider_item_id,
46
        OLD.provider_response_id, OLD.tool_name, OLD.tool_version,
47
        OLD.module_id, OLD.catalog_digest, #{old_raw} OLD.argument_digest,
48
        OLD.requested_at
49
      ) IS DISTINCT FROM ROW(
50
        NEW.voice_session_id, NEW.voice_response_receipt_id, NEW.generation,
51
        NEW.sequence, NEW.provider_call_id, NEW.provider_item_id,
52
        NEW.provider_response_id, NEW.tool_name, NEW.tool_version,
53
        NEW.module_id, NEW.catalog_digest, #{new_raw} NEW.argument_digest,
54
        NEW.requested_at
55
      ) THEN
56
        RAISE EXCEPTION 'voice tool step identity is immutable';
57
      END IF;
58
59
      IF OLD.status = 'requested' AND NEW.status NOT IN (
60
        'requested', 'running', 'succeeded', 'failed', 'refused',
61
        'cancelled', 'unavailable', 'interrupted'
62
      ) THEN
63
        RAISE EXCEPTION 'invalid requested voice tool step transition';
64
      END IF;
65
66
      IF OLD.status = 'running' AND NEW.status NOT IN (
67
        'running', 'succeeded', 'failed', 'refused', 'cancelled',
68
        'unavailable', 'interrupted'
69
      ) THEN
70
        RAISE EXCEPTION 'invalid running voice tool step transition';
71
      END IF;
72
73
      IF OLD.status NOT IN ('requested', 'running') AND ROW(
74
        OLD.status, OLD.outcome_digest, OLD.result, OLD.error,
75
        OLD.executor_id, OLD.executor_disclosure, OLD.target_receipt_refs,
76
        OLD.attribution_refs, OLD.started_at, OLD.completed_at
77
      ) IS DISTINCT FROM ROW(
78
        NEW.status, NEW.outcome_digest, NEW.result, NEW.error,
79
        NEW.executor_id, NEW.executor_disclosure, NEW.target_receipt_refs,
80
        NEW.attribution_refs, NEW.started_at, NEW.completed_at
81
      ) THEN
82
        RAISE EXCEPTION 'terminal voice tool step is immutable';
83
      END IF;
84
85
      RETURN NEW;
86
    END;
87
    $$ LANGUAGE plpgsql;
88
    """)
89
  end
90
end
priv/repo/migrations/20260820021249_create_voice_response_contexts.exs deleted -7

@@ -1,7 +0,0 @@

1
defmodule OpenAgents.Repo.Migrations.CreateVoiceResponseContexts do
2
  use Ecto.Migration
3
4
  def change do
5
6
  end
7
end
priv/repo/migrations/20260820021249_create_voice_tool_steps.exs deleted -7

@@ -1,7 +0,0 @@

1
defmodule OpenAgents.Repo.Migrations.CreateVoiceToolSteps do
2
  use Ecto.Migration
3
4
  def change do
5
6
  end
7
end

This page updates live while a promote is in flight · changelog