@sixb/pg 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8015,6 +8015,159 @@ CREATE INDEX idx_ontology_sources_drained
8015
8015
  WHERE status = 'superseded' AND root_count = 0;
8016
8016
  `;
8017
8017
 
8018
+ // src/migrations/061-terminal-action-runs.sql
8019
+ var _061_terminal_action_runs_default = `-- Actions run in the request that asks for them and record their run once, when it ends. No run
8020
+ -- is queued, running, or cancelled any more, and every run has started and finished.
8021
+
8022
+ -- A run left running after its edits committed, as a crash between the commit and the end of the
8023
+ -- run left it, succeeded: its edits and their outbox facts are in the commit. A run that stopped
8024
+ -- during its effects keeps that phase, and records effects that never ended.
8025
+ UPDATE action_runs AS run
8026
+ SET
8027
+ status = 'succeeded',
8028
+ error = NULL,
8029
+ finished_at = committed.committed_at,
8030
+ phase = CASE WHEN run.phase = 'effects' THEN 'effects' ELSE 'commit' END,
8031
+ effects_status = CASE
8032
+ WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN 'failed'
8033
+ ELSE run.effects_status
8034
+ END,
8035
+ effects_completed_at = CASE
8036
+ WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN now()
8037
+ ELSE run.effects_completed_at
8038
+ END,
8039
+ effects_error = CASE
8040
+ WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN jsonb_build_object(
8041
+ 'code', 'internal.unexpected',
8042
+ 'message', 'Abandoned: Actions now run synchronously; these effects never finished.',
8043
+ 'retryable', false,
8044
+ 'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
8045
+ 'details', jsonb_build_object('actionId', run.action_id, 'runId', run.id, 'phase', 'effects')
8046
+ )
8047
+ ELSE run.effects_error
8048
+ END
8049
+ FROM ontology_commits AS committed
8050
+ WHERE run.status IN ('queued', 'running')
8051
+ AND committed.origin_kind = 'action'
8052
+ AND committed.project_id = run.project_id
8053
+ AND committed.origin_run_id = run.id;
8054
+
8055
+ -- Any other run left queued or running never finished, and nothing will resume it.
8056
+ UPDATE action_runs
8057
+ SET
8058
+ status = 'failed',
8059
+ finished_at = now(),
8060
+ error = jsonb_build_object(
8061
+ 'code', 'internal.unexpected',
8062
+ 'message', 'Abandoned: Actions now run synchronously; this run never finished.',
8063
+ 'retryable', false,
8064
+ 'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
8065
+ 'details', jsonb_build_object('actionId', action_id, 'runId', id)
8066
+ )
8067
+ WHERE status IN ('queued', 'running');
8068
+
8069
+ -- A cancelled run failed: it keeps the failure it recorded.
8070
+ UPDATE action_runs SET status = 'failed' WHERE status = 'cancelled';
8071
+
8072
+ -- Every failed run carries a failure. One recorded before failures were kept explains itself.
8073
+ UPDATE action_runs
8074
+ SET error = jsonb_build_object(
8075
+ 'code', 'internal.unexpected',
8076
+ 'message', 'This run failed before Sixb recorded why.',
8077
+ 'retryable', false,
8078
+ 'at', to_char(COALESCE(finished_at, now()) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
8079
+ 'details', jsonb_build_object('actionId', action_id, 'runId', id)
8080
+ )
8081
+ WHERE status = 'failed' AND error IS NULL;
8082
+
8083
+ -- The phases \`request\`, \`enqueue\`, and \`cancelled\` are gone: a run that stopped in one of them
8084
+ -- never got past validation. A run starts when it was queued at the latest.
8085
+ UPDATE action_runs
8086
+ SET
8087
+ phase = CASE
8088
+ WHEN phase IN ('validation', 'writeback', 'edits', 'commit', 'effects') THEN phase
8089
+ ELSE 'validation'
8090
+ END,
8091
+ started_at = COALESCE(started_at, queued_at),
8092
+ finished_at = COALESCE(finished_at, now()),
8093
+ writeback_completed_at = CASE
8094
+ WHEN writeback_status IS NULL THEN NULL
8095
+ ELSE COALESCE(writeback_completed_at, finished_at, now())
8096
+ END,
8097
+ effects_completed_at = CASE
8098
+ WHEN effects_status IS NULL THEN NULL
8099
+ ELSE COALESCE(effects_completed_at, finished_at, now())
8100
+ END
8101
+ WHERE phase IS NULL
8102
+ OR phase NOT IN ('validation', 'writeback', 'edits', 'commit', 'effects')
8103
+ OR started_at IS NULL
8104
+ OR finished_at IS NULL
8105
+ OR (writeback_status IS NOT NULL AND writeback_completed_at IS NULL)
8106
+ OR (effects_status IS NOT NULL AND effects_completed_at IS NULL);
8107
+
8108
+ -- A failure names the phase its run ended in, and only Action codes: an enqueue failure, which no
8109
+ -- run can meet any more, reads as the unexpected error it now is.
8110
+ UPDATE action_runs
8111
+ SET error = jsonb_set(
8112
+ CASE
8113
+ WHEN error->>'code' = 'queue.enqueue_failed'
8114
+ THEN error || '{"code": "internal.unexpected", "retryable": false}'::jsonb
8115
+ ELSE error
8116
+ END,
8117
+ '{details,phase}',
8118
+ to_jsonb(phase)
8119
+ )
8120
+ WHERE error IS NOT NULL
8121
+ AND (
8122
+ error->'details'->>'phase' IS DISTINCT FROM phase
8123
+ OR error->>'code' = 'queue.enqueue_failed'
8124
+ );
8125
+
8126
+ -- The status and phase checks were unnamed. Drop exactly the two that admit the removed values.
8127
+ DO $$
8128
+ DECLARE lifecycle_constraint RECORD;
8129
+ BEGIN
8130
+ FOR lifecycle_constraint IN
8131
+ SELECT conname
8132
+ FROM pg_constraint
8133
+ WHERE conrelid = 'action_runs'::regclass
8134
+ AND contype = 'c'
8135
+ AND (
8136
+ pg_get_constraintdef(oid) LIKE '%''queued''%'
8137
+ OR pg_get_constraintdef(oid) LIKE '%''enqueue''%'
8138
+ )
8139
+ LOOP
8140
+ EXECUTE format('ALTER TABLE action_runs DROP CONSTRAINT %I', lifecycle_constraint.conname);
8141
+ END LOOP;
8142
+ END $$;
8143
+
8144
+ -- The listing indexes ordered runs by \`COALESCE(started_at, queued_at)\`.
8145
+ DROP INDEX idx_action_runs_project_started;
8146
+ DROP INDEX idx_action_runs_project_action_started;
8147
+ DROP INDEX idx_action_runs_project_object_started;
8148
+ DROP INDEX idx_action_runs_project_status_started;
8149
+
8150
+ ALTER TABLE action_runs
8151
+ DROP COLUMN queued_at,
8152
+ ALTER COLUMN phase SET NOT NULL,
8153
+ ALTER COLUMN started_at SET NOT NULL,
8154
+ ALTER COLUMN finished_at SET NOT NULL,
8155
+ ADD CONSTRAINT action_runs_status_check CHECK (status IN ('succeeded', 'failed')),
8156
+ ADD CONSTRAINT action_runs_phase_check CHECK (
8157
+ phase IN ('validation', 'writeback', 'edits', 'commit', 'effects')
8158
+ ),
8159
+ ADD CONSTRAINT action_runs_failure_check CHECK ((status = 'failed') = (error IS NOT NULL));
8160
+
8161
+ CREATE INDEX idx_action_runs_project_started
8162
+ ON action_runs (project_id, started_at DESC);
8163
+ CREATE INDEX idx_action_runs_project_action_started
8164
+ ON action_runs (project_id, action_id, started_at DESC);
8165
+ CREATE INDEX idx_action_runs_project_object_started
8166
+ ON action_runs (project_id, object_type_id, primary_id, started_at DESC);
8167
+ CREATE INDEX idx_action_runs_project_status_started
8168
+ ON action_runs (project_id, status, started_at DESC);
8169
+ `;
8170
+
8018
8171
  // src/migrations.ts
8019
8172
  var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
8020
8173
  function pgSql(id, sqlText) {
@@ -8241,7 +8394,8 @@ var postgresStorageMigrations = defineMigrations({
8241
8394
  pgSql("057-audio-transcription", _057_audio_transcription_default),
8242
8395
  pgSql("058-reranking-model-kind", _058_reranking_model_kind_default),
8243
8396
  pgSql("059-file-download-grants", _059_file_download_grants_default),
8244
- pgSql("060-drained-source-versions", _060_drained_source_versions_default)
8397
+ pgSql("060-drained-source-versions", _060_drained_source_versions_default),
8398
+ pgSql("061-terminal-action-runs", _061_terminal_action_runs_default)
8245
8399
  ]
8246
8400
  });
8247
8401
 
@@ -15681,12 +15835,8 @@ import {
15681
15835
  serializeActionRunFailure
15682
15836
  } from "@sixb/core/internal/action-run-storage";
15683
15837
  import { assertActionRunExecution } from "@sixb/core/internal/action-run-storage-provider";
15684
- import {
15685
- actionRunPhaseRecordsEqual,
15686
- canRequeueActionRunAfterEnqueueFailure,
15687
- finishActionRunPhase
15688
- } from "@sixb/core/internal/storage";
15689
- import { ActionRunError, isTerminalActionRun } from "@sixb/core/storage";
15838
+ import { normalizeRecordActionRunInput, resolveActionRunEffects } from "@sixb/core/internal/storage";
15839
+ import { ActionRunError } from "@sixb/core/storage";
15690
15840
  class PgActionRunStorage {
15691
15841
  sql;
15692
15842
  executions;
@@ -15694,227 +15844,83 @@ class PgActionRunStorage {
15694
15844
  this.sql = sql;
15695
15845
  this.executions = executions;
15696
15846
  }
15697
- async lockForMaterialization(input) {
15698
- const [row] = await this.sql`
15699
- SELECT *
15700
- FROM action_runs
15701
- WHERE project_id = ${input.projectId} AND id = ${input.runId}
15702
- FOR UPDATE
15703
- `;
15704
- if (!row) {
15705
- throw new ActionRunError(`[SixbPg] Action run '${input.runId}' not found for project '${input.projectId}'.`);
15706
- }
15707
- if (row.action_id !== input.actionId) {
15708
- throw new ActionRunError(`[SixbPg] Action run '${input.runId}' does not belong to action '${input.actionId}'.`);
15709
- }
15710
- if (row.status !== "running") {
15711
- throw new ActionRunError(`[SixbPg] Action run '${input.runId}' cannot materialize from status '${row.status}'.`);
15712
- }
15713
- return rowToActionRunRecord(row);
15714
- }
15715
- async queue(input) {
15847
+ async record(input) {
15848
+ const record = normalizeRecordActionRunInput(input);
15716
15849
  await assertActionRunExecution({
15717
15850
  executions: this.executions,
15718
- projectId: input.projectId,
15719
- executionId: input.executionId,
15720
- runId: input.id,
15721
- actionId: input.actionId
15722
- });
15723
- try {
15724
- const [row] = await this.sql`
15725
- INSERT INTO action_runs (
15726
- project_id,
15727
- id,
15728
- execution_id,
15729
- action_id,
15730
- subject_kind,
15731
- object_type_id,
15732
- primary_id,
15733
- status,
15734
- phase,
15735
- queued_at,
15736
- started_at,
15737
- finished_at,
15738
- params,
15739
- idempotency_key,
15740
- writeback_status,
15741
- writeback_completed_at,
15742
- writeback_result,
15743
- writeback_error,
15744
- effects_status,
15745
- effects_completed_at,
15746
- effects_error,
15747
- error
15748
- ) VALUES (
15749
- ${input.projectId},
15750
- ${input.id},
15751
- ${input.executionId},
15752
- ${input.actionId},
15753
- ${input.subject.kind},
15754
- ${input.subject.kind === "object" ? input.subject.objectTypeId : null},
15755
- ${input.subject.kind === "object" ? input.subject.primaryId : null},
15756
- ${"queued"},
15757
- ${"request"},
15758
- ${input.queuedAt ?? new Date},
15759
- ${null},
15760
- ${null},
15761
- ${JSON.stringify(input.params)}::text::jsonb,
15762
- ${input.idempotencyKey},
15763
- ${null},
15764
- ${null},
15765
- ${null},
15766
- ${null},
15767
- ${null},
15768
- ${null},
15769
- ${null},
15770
- ${null}
15771
- )
15772
- RETURNING *
15773
- `;
15774
- return rowToActionRunRecord(row);
15775
- } catch (error) {
15776
- if (isUniqueViolation(error)) {
15777
- return this.requeueAfterEnqueueFailure(input);
15778
- }
15779
- throw error;
15780
- }
15781
- }
15782
- async requeueAfterEnqueueFailure(input) {
15783
- return runPgTransaction(this.sql, async (tx) => {
15784
- const [existing] = await tx`
15785
- SELECT * FROM action_runs
15786
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15787
- FOR UPDATE
15788
- `;
15789
- if (!existing || !canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)) {
15790
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' already exists for project '${input.projectId}'.`);
15791
- }
15792
- const [updated] = await tx`
15793
- UPDATE action_runs
15794
- SET
15795
- status = ${"queued"},
15796
- phase = ${"request"},
15797
- queued_at = ${input.queuedAt ?? new Date},
15798
- started_at = ${null},
15799
- finished_at = ${null},
15800
- writeback_status = ${null},
15801
- writeback_completed_at = ${null},
15802
- writeback_result = ${null},
15803
- writeback_error = ${null},
15804
- effects_status = ${null},
15805
- effects_completed_at = ${null},
15806
- effects_error = ${null},
15807
- error = ${null}
15808
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15809
- RETURNING *
15810
- `;
15811
- return rowToActionRunRecord(updated);
15851
+ projectId: record.projectId,
15852
+ executionId: record.executionId,
15853
+ runId: record.id,
15854
+ actionId: record.actionId
15812
15855
  });
15813
- }
15814
- async start(input) {
15815
- const [updated] = await this.sql`
15816
- UPDATE action_runs
15817
- SET
15818
- status = ${"running"},
15819
- phase = ${input.phase ?? "validation"},
15820
- started_at = ${input.startedAt ?? new Date},
15821
- error = ${null}
15822
- WHERE project_id = ${input.projectId}
15823
- AND id = ${input.id}
15824
- AND status = ${"queued"}
15856
+ const [row] = await this.sql`
15857
+ INSERT INTO action_runs (
15858
+ project_id,
15859
+ id,
15860
+ execution_id,
15861
+ action_id,
15862
+ subject_kind,
15863
+ object_type_id,
15864
+ primary_id,
15865
+ status,
15866
+ phase,
15867
+ started_at,
15868
+ finished_at,
15869
+ params,
15870
+ idempotency_key,
15871
+ writeback_status,
15872
+ writeback_completed_at,
15873
+ writeback_result,
15874
+ writeback_error,
15875
+ error
15876
+ ) VALUES (
15877
+ ${record.projectId},
15878
+ ${record.id},
15879
+ ${record.executionId},
15880
+ ${record.actionId},
15881
+ ${record.subject.kind},
15882
+ ${record.subject.kind === "object" ? record.subject.objectTypeId : null},
15883
+ ${record.subject.kind === "object" ? record.subject.primaryId : null},
15884
+ ${record.status},
15885
+ ${record.phase},
15886
+ ${record.startedAt},
15887
+ ${record.finishedAt},
15888
+ ${JSON.stringify(record.params)}::text::jsonb,
15889
+ ${record.idempotencyKey},
15890
+ ${record.writeback?.status ?? null},
15891
+ ${record.writeback?.completedAt ?? null},
15892
+ ${record.writeback?.status === "succeeded" ? JSON.stringify(record.writeback.result) : null}::text::jsonb,
15893
+ ${record.writeback?.status === "failed" ? serializeActionRunFailure(record.writeback.error, "writeback") : null}::text::jsonb,
15894
+ ${record.status === "failed" ? serializeActionRunFailure(record.error) : null}::text::jsonb
15895
+ )
15896
+ ON CONFLICT DO NOTHING
15825
15897
  RETURNING *
15826
15898
  `;
15827
- if (updated) {
15828
- return rowToActionRunRecord(updated);
15829
- }
15830
- const existing = await this.getById({ projectId: input.projectId, id: input.id });
15831
- if (!existing) {
15832
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`);
15833
- }
15834
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' cannot start from status '${existing.status}'.`);
15835
- }
15836
- async enterPhase(input) {
15837
- return runPgTransaction(this.sql, async (tx) => {
15838
- await this.requireRunningRun(tx, input.projectId, input.id, "transition phase");
15839
- const [updated] = await tx`
15840
- UPDATE action_runs
15841
- SET phase = ${input.phase}
15842
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15843
- RETURNING *
15844
- `;
15845
- return rowToActionRunRecord(updated);
15846
- });
15847
- }
15848
- async recordWriteback(input) {
15849
- return runPgTransaction(this.sql, async (tx) => {
15850
- const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record writeback");
15851
- const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date));
15852
- const currentWriteback = toActionRunWritebackRecord(existing);
15853
- if (currentWriteback) {
15854
- if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
15855
- return rowToActionRunRecord(existing);
15856
- }
15857
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' already has a different writeback record.`);
15858
- }
15859
- const [updated] = await tx`
15860
- UPDATE action_runs
15861
- SET
15862
- phase = ${"writeback"},
15863
- writeback_status = ${input.status},
15864
- writeback_completed_at = ${nextWriteback.completedAt},
15865
- writeback_result = ${input.status === "succeeded" ? JSON.stringify(input.result) : null}::text::jsonb,
15866
- writeback_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "writeback") : null}::text::jsonb
15867
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15868
- RETURNING *
15869
- `;
15870
- return rowToActionRunRecord(updated);
15871
- });
15899
+ if (row)
15900
+ return rowToActionRunRecord(row);
15901
+ const [conflict] = await this.sql`
15902
+ SELECT id FROM action_runs
15903
+ WHERE project_id = ${record.projectId} AND id = ${record.id}
15904
+ `;
15905
+ throw new ActionRunError(conflict ? `[SixbPg] Action run '${record.id}' is already recorded for project '${record.projectId}'.` : `[SixbPg] Execution '${record.executionId}' already belongs to another Action run.`);
15872
15906
  }
15873
15907
  async recordEffects(input) {
15874
- return runPgTransaction(this.sql, async (tx) => {
15875
- const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record effects");
15876
- const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date));
15877
- const currentEffects = toActionRunEffectsRecord(existing);
15878
- if (currentEffects) {
15879
- if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
15880
- return rowToActionRunRecord(existing);
15881
- }
15882
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' already has a different effects record.`);
15883
- }
15884
- const [updated] = await tx`
15885
- UPDATE action_runs
15886
- SET
15887
- phase = ${"effects"},
15888
- effects_status = ${input.status},
15889
- effects_completed_at = ${nextEffects.completedAt},
15890
- effects_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "effects") : null}::text::jsonb
15891
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15892
- RETURNING *
15893
- `;
15894
- return rowToActionRunRecord(updated);
15895
- });
15896
- }
15897
- async finish(input) {
15898
15908
  return runPgTransaction(this.sql, async (tx) => {
15899
15909
  const [existing] = await tx`
15900
15910
  SELECT * FROM action_runs
15901
15911
  WHERE project_id = ${input.projectId} AND id = ${input.id}
15902
15912
  FOR UPDATE
15903
15913
  `;
15904
- if (!existing) {
15905
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`);
15906
- }
15907
- if (isTerminalActionRun({ status: existing.status })) {
15908
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`);
15909
- }
15910
- const phase = finishActionRunPhase(input, existing.phase);
15914
+ const { run, effects } = resolveActionRunEffects(existing ? rowToActionRunRecord(existing) : null, input);
15915
+ if (!effects)
15916
+ return run;
15911
15917
  const [updated] = await tx`
15912
15918
  UPDATE action_runs
15913
15919
  SET
15914
- status = ${input.status},
15915
- phase = ${phase},
15916
- finished_at = ${input.finishedAt ?? new Date},
15917
- error = ${input.status === "succeeded" ? null : serializeActionRunFailure(input.error)}::text::jsonb
15920
+ phase = ${"effects"},
15921
+ effects_status = ${effects.status},
15922
+ effects_completed_at = ${effects.completedAt},
15923
+ effects_error = ${effects.status === "failed" ? serializeActionRunFailure(effects.error, "effects") : null}::text::jsonb
15918
15924
  WHERE project_id = ${input.projectId} AND id = ${input.id}
15919
15925
  RETURNING *
15920
15926
  `;
@@ -15987,11 +15993,11 @@ class PgActionRunStorage {
15987
15993
  params.push(...input.statuses);
15988
15994
  }
15989
15995
  if (input.startedAfter) {
15990
- whereClauses.push(`COALESCE(started_at, queued_at) >= $${index++}`);
15996
+ whereClauses.push(`started_at >= $${index++}`);
15991
15997
  params.push(input.startedAfter);
15992
15998
  }
15993
15999
  if (input.startedBefore) {
15994
- whereClauses.push(`COALESCE(started_at, queued_at) <= $${index++}`);
16000
+ whereClauses.push(`started_at <= $${index++}`);
15995
16001
  params.push(input.startedBefore);
15996
16002
  }
15997
16003
  const where = `WHERE ${whereClauses.join(" AND ")}`;
@@ -16002,7 +16008,7 @@ class PgActionRunStorage {
16002
16008
  let query = `
16003
16009
  SELECT * FROM action_runs
16004
16010
  ${where}
16005
- ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
16011
+ ORDER BY started_at ${order}, id ${order}
16006
16012
  `;
16007
16013
  if (input.limit !== undefined) {
16008
16014
  query += ` LIMIT $${index++} OFFSET $${index++}`;
@@ -16020,32 +16026,12 @@ class PgActionRunStorage {
16020
16026
  total
16021
16027
  };
16022
16028
  }
16023
- async requireRunningRun(runner, projectId, id, operation) {
16024
- const [existing] = await runner`
16025
- SELECT * FROM action_runs
16026
- WHERE project_id = ${projectId} AND id = ${id}
16027
- FOR UPDATE
16028
- `;
16029
- if (!existing) {
16030
- throw new ActionRunError(`[SixbPg] Action run '${id}' not found for project '${projectId}'.`);
16031
- }
16032
- if (existing.status !== "running") {
16033
- throw new ActionRunError(`[SixbPg] Action run '${id}' cannot ${operation} from status '${existing.status}'.`);
16034
- }
16035
- return existing;
16036
- }
16037
- }
16038
- function toActionRunFailure(row) {
16039
- return row.error === null ? undefined : parseActionRunFailure(row.error);
16040
- }
16041
- function parsePhaseFailure(value, expectedPhase) {
16042
- return parseActionRunFailure(value, expectedPhase);
16043
16029
  }
16044
16030
  function toActionRunWritebackRecord(row) {
16045
16031
  if (!row.writeback_status) {
16046
16032
  return;
16047
16033
  }
16048
- const completedAt = row.writeback_completed_at ? new Date(row.writeback_completed_at) : new Date(row.queued_at);
16034
+ const completedAt = new Date(row.writeback_completed_at ?? row.finished_at);
16049
16035
  if (row.writeback_status === "succeeded") {
16050
16036
  return {
16051
16037
  status: "succeeded",
@@ -16056,14 +16042,14 @@ function toActionRunWritebackRecord(row) {
16056
16042
  return {
16057
16043
  status: "failed",
16058
16044
  completedAt,
16059
- error: parsePhaseFailure(row.writeback_error, "writeback")
16045
+ error: parseActionRunFailure(row.writeback_error, "writeback")
16060
16046
  };
16061
16047
  }
16062
16048
  function toActionRunEffectsRecord(row) {
16063
16049
  if (!row.effects_status) {
16064
16050
  return;
16065
16051
  }
16066
- const completedAt = row.effects_completed_at ? new Date(row.effects_completed_at) : new Date(row.queued_at);
16052
+ const completedAt = new Date(row.effects_completed_at ?? row.finished_at);
16067
16053
  if (row.effects_status === "succeeded") {
16068
16054
  return {
16069
16055
  status: "succeeded",
@@ -16073,54 +16059,29 @@ function toActionRunEffectsRecord(row) {
16073
16059
  return {
16074
16060
  status: "failed",
16075
16061
  completedAt,
16076
- error: parsePhaseFailure(row.effects_error, "effects")
16077
- };
16078
- }
16079
- function toWritebackRecord(input, completedAt) {
16080
- if (input.status === "succeeded") {
16081
- return {
16082
- status: "succeeded",
16083
- completedAt,
16084
- result: input.result
16085
- };
16086
- }
16087
- return {
16088
- status: "failed",
16089
- completedAt,
16090
- error: input.error
16091
- };
16092
- }
16093
- function toEffectsRecord(input, completedAt) {
16094
- if (input.status === "succeeded") {
16095
- return {
16096
- status: "succeeded",
16097
- completedAt
16098
- };
16099
- }
16100
- return {
16101
- status: "failed",
16102
- completedAt,
16103
- error: input.error
16062
+ error: parseActionRunFailure(row.effects_error, "effects")
16104
16063
  };
16105
16064
  }
16106
16065
  function rowToActionRunRecord(row) {
16107
- return {
16066
+ const writeback = toActionRunWritebackRecord(row);
16067
+ const effects = toActionRunEffectsRecord(row);
16068
+ const fields = {
16108
16069
  id: row.id,
16109
16070
  projectId: row.project_id,
16110
16071
  executionId: row.execution_id,
16111
16072
  actionId: row.action_id,
16112
16073
  subject: rowToActionSubject(row),
16113
- status: row.status,
16114
- phase: row.phase ?? undefined,
16115
- queuedAt: new Date(row.queued_at),
16116
- startedAt: row.started_at ? new Date(row.started_at) : undefined,
16117
- finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
16074
+ phase: row.phase,
16075
+ startedAt: new Date(row.started_at),
16076
+ finishedAt: new Date(row.finished_at),
16118
16077
  params: row.params,
16119
16078
  idempotencyKey: row.idempotency_key,
16120
- writeback: toActionRunWritebackRecord(row),
16121
- effects: toActionRunEffectsRecord(row),
16122
- error: toActionRunFailure(row)
16079
+ ...writeback === undefined ? {} : { writeback },
16080
+ ...effects === undefined ? {} : { effects }
16123
16081
  };
16082
+ if (row.status === "succeeded")
16083
+ return { ...fields, status: "succeeded" };
16084
+ return { ...fields, status: "failed", error: parseActionRunFailure(row.error) };
16124
16085
  }
16125
16086
  function rowToActionSubject(row) {
16126
16087
  if (row.subject_kind === "none") {
@@ -21065,4 +21026,4 @@ export {
21065
21026
  migratePostgresStorage
21066
21027
  };
21067
21028
 
21068
- //# debugId=51821498ED16AC6B64756E2164756E21
21029
+ //# debugId=295D429EF5EE4F7864756E2164756E21