@consiliency/agent-board-schema 1.23.0 → 1.25.0
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/README.md +75 -1
- package/fixtures/already-applied.json +12 -0
- package/migrations/20260905000100_agent_board_consent_gating_reconcile.sql +151 -0
- package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
- package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
- package/package.json +1 -1
- package/schema-manifest.json +30 -3
- package/src/check.js +23 -0
- package/src/index.js +20 -0
- package/src/plan.js +79 -5
- package/src/tooling.test.mjs +237 -4
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +124 -112
- package/tests/agent_board_claimkey.test.sql +25 -3
- package/tests/agent_board_consent_emission.test.sql +17 -13
- package/tests/agent_board_consent_gating.test.sql +55 -11
- package/tests/agent_board_delegation_policies.test.sql +3 -1
- package/tests/agent_board_enroll_resumption.test.sql +412 -0
- package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
- package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
- package/tests/agent_board_mint_harden.test.sql +39 -4
- package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
- package/tests/agent_board_rejectsem.test.sql +1 -1
- package/tests/agent_board_rolereset.test.sql +23 -2
- package/tests/agent_board_rowid.test.sql +24 -13
- package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
- package/tests/agent_board_signed_authority_provenance.test.sql +454 -0
- package/tests/agent_board_source_strip_helper.test.sql +285 -0
- package/tests/agent_board_wake_recovery.test.sql +58 -40
- package/tests/catalog/p_metadata_parameters.json +689 -0
- package/tests/fixtures/test-prereqs.sql +313 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
|
@@ -115,3 +115,316 @@ $$;
|
|
|
115
115
|
REVOKE ALL ON FUNCTION public.test_pubkey(text) FROM PUBLIC;
|
|
116
116
|
REVOKE ALL ON FUNCTION public.test_key_fingerprint(text) FROM PUBLIC;
|
|
117
117
|
GRANT EXECUTE ON FUNCTION public.test_pubkey(text), public.test_key_fingerprint(text) TO service_role;
|
|
118
|
+
|
|
119
|
+
-- ===========================================================================
|
|
120
|
+
-- Shared SQL source-text strip for pgTAP source assertions
|
|
121
|
+
-- (message-board#196, message-board#199).
|
|
122
|
+
--
|
|
123
|
+
-- WHY THIS EXISTS. A pgTAP assertion that locates a marker in a function body
|
|
124
|
+
-- with `strpos(...)` and compares positions, or that claims a marker is ABSENT
|
|
125
|
+
-- with `!~` / `not like`, reads AUTHOR-CONTROLLED text. Prose that merely
|
|
126
|
+
-- MENTIONS the marker satisfies the assertion while the code does the opposite,
|
|
127
|
+
-- so the guard reports "ordered" / "absent" when it means "my pattern matched
|
|
128
|
+
-- something that is not code". Every mode below was DEMONSTRATED, not reasoned
|
|
129
|
+
-- about; each one left a hardened file fully green with a lock genuinely moved.
|
|
130
|
+
--
|
|
131
|
+
-- ENUMERATED ESCAPE HATCHES, and which of them a regex can close:
|
|
132
|
+
-- 1. a `--` comment naming the marker -- regex can
|
|
133
|
+
-- 2. a block comment whose body ends in `*` (`/* … **/`) -- regex can
|
|
134
|
+
-- 3. a string literal naming the marker, incl. a dollar-quoted
|
|
135
|
+
-- one; a naive two-pass scanner also DESYNCS on an odd
|
|
136
|
+
-- apostrophe count (`mark_retention_metadata`'s $retention$
|
|
137
|
+
-- block survives one today only because its 64 apostrophes
|
|
138
|
+
-- happen to be an even number) -- regex can
|
|
139
|
+
-- 4. a NESTED block comment (message-board#199). Postgres block
|
|
140
|
+
-- comments NEST; `/* note /* x */ <code> */` hides <code>
|
|
141
|
+
-- from any regex that stops at the first `*/` -- REGEX CANNOT
|
|
142
|
+
-- 5. an apostrophe inside a "quoted identifier"; a regex whose
|
|
143
|
+
-- literal alternative does not know about `"…"` desyncs on
|
|
144
|
+
-- it exactly as (3) desyncs -- regex cannot
|
|
145
|
+
--
|
|
146
|
+
-- (4) is why this is a character scanner and not a cleverer regular expression.
|
|
147
|
+
-- Postgres' regex engine has no recursion, so no regular expression can match a
|
|
148
|
+
-- nested block comment: the language is not regular. Rejecting nested comments
|
|
149
|
+
-- instead of handling them was considered (message-board#199 suggests it) and
|
|
150
|
+
-- not taken -- a scanner that handles nesting is exact, is unit-testable
|
|
151
|
+
-- (agent_board_source_strip_helper.test.sql), and does not have to guess which
|
|
152
|
+
-- constructs are "legitimate".
|
|
153
|
+
--
|
|
154
|
+
-- ONE PASS, NOT TWO. Comments-first eats a literal containing `--`;
|
|
155
|
+
-- literals-first eats a comment containing an apostrophe. This scanner consumes
|
|
156
|
+
-- whichever token OPENS first, which is the correct tokenization and the only
|
|
157
|
+
-- one that cannot desync.
|
|
158
|
+
--
|
|
159
|
+
-- FAIL CLOSED. An unterminated comment, literal, dollar quote or quoted
|
|
160
|
+
-- identifier raises rather than returning a truncated body, so the failure is
|
|
161
|
+
-- loud instead of a silently short string that every `!~` passes.
|
|
162
|
+
--
|
|
163
|
+
-- MODES. `p_keep_literals => false` (the default) is the safe one and is what a
|
|
164
|
+
-- positional or negative assertion should use. `p_keep_literals => true` keeps
|
|
165
|
+
-- string literals -- required ONLY where the MARKER ITSELF IS A LITERAL (e.g.
|
|
166
|
+
-- an assertion locating a `raise exception '<message>'`), because stripping the
|
|
167
|
+
-- literals there would make the assertion vacuously true forever. Literals are
|
|
168
|
+
-- still CONSUMED in that mode, so a `--` or `/*` inside one cannot be mistaken
|
|
169
|
+
-- for a comment opener. This choice is NOT machine-checkable -- the lint in
|
|
170
|
+
-- scripts/run-agent-board-schema-tests.sh cannot tell a message anchor from a
|
|
171
|
+
-- code anchor -- so every `p_keep_literals => true` call site carries an inline
|
|
172
|
+
-- justification.
|
|
173
|
+
-- ===========================================================================
|
|
174
|
+
CREATE SCHEMA IF NOT EXISTS agent_board_test;
|
|
175
|
+
|
|
176
|
+
CREATE OR REPLACE FUNCTION agent_board_test.strip_sql(
|
|
177
|
+
p_text text,
|
|
178
|
+
p_keep_literals boolean DEFAULT false
|
|
179
|
+
) RETURNS text
|
|
180
|
+
LANGUAGE plpgsql
|
|
181
|
+
IMMUTABLE
|
|
182
|
+
SET search_path = pg_catalog
|
|
183
|
+
AS $strip$
|
|
184
|
+
declare
|
|
185
|
+
c text[]; -- p_text as characters; array indexing is O(1),
|
|
186
|
+
-- substr()-per-character on a multibyte text is not
|
|
187
|
+
n int;
|
|
188
|
+
i int := 1;
|
|
189
|
+
span int := 1; -- start of the current KEPT run
|
|
190
|
+
parts text[] := '{}';
|
|
191
|
+
depth int;
|
|
192
|
+
j int;
|
|
193
|
+
tag text;
|
|
194
|
+
close_at int;
|
|
195
|
+
is_estring boolean;
|
|
196
|
+
begin
|
|
197
|
+
if p_text is null then
|
|
198
|
+
return null;
|
|
199
|
+
end if;
|
|
200
|
+
|
|
201
|
+
c := regexp_split_to_array(p_text, '');
|
|
202
|
+
n := coalesce(array_length(c, 1), 0);
|
|
203
|
+
if n = 0 then
|
|
204
|
+
return p_text;
|
|
205
|
+
end if;
|
|
206
|
+
|
|
207
|
+
while i <= n loop
|
|
208
|
+
|
|
209
|
+
-- (1) line comment ------------------------------------------------------
|
|
210
|
+
if c[i] = '-' and i < n and c[i + 1] = '-' then
|
|
211
|
+
parts := parts || substr(p_text, span, i - span);
|
|
212
|
+
while i <= n and c[i] <> chr(10) loop
|
|
213
|
+
i := i + 1;
|
|
214
|
+
end loop;
|
|
215
|
+
span := i; -- the newline itself is code-relevant whitespace; keep it
|
|
216
|
+
|
|
217
|
+
-- (2) block comment, WITH NESTING (message-board#199) --------------------
|
|
218
|
+
elsif c[i] = '/' and i < n and c[i + 1] = '*' then
|
|
219
|
+
parts := parts || substr(p_text, span, i - span);
|
|
220
|
+
depth := 1;
|
|
221
|
+
i := i + 2;
|
|
222
|
+
while i <= n and depth > 0 loop
|
|
223
|
+
if c[i] = '/' and i < n and c[i + 1] = '*' then
|
|
224
|
+
depth := depth + 1; i := i + 2;
|
|
225
|
+
elsif c[i] = '*' and i < n and c[i + 1] = '/' then
|
|
226
|
+
depth := depth - 1; i := i + 2;
|
|
227
|
+
else
|
|
228
|
+
i := i + 1;
|
|
229
|
+
end if;
|
|
230
|
+
end loop;
|
|
231
|
+
if depth > 0 then
|
|
232
|
+
raise exception
|
|
233
|
+
'agent_board_test.strip_sql: unterminated block comment (depth %) -- refusing to return a truncated body',
|
|
234
|
+
depth;
|
|
235
|
+
end if;
|
|
236
|
+
span := i;
|
|
237
|
+
|
|
238
|
+
-- (3) quoted identifier: CODE, kept verbatim, but CONSUMED so that an
|
|
239
|
+
-- apostrophe inside it cannot desync the literal scanner ------------
|
|
240
|
+
elsif c[i] = '"' then
|
|
241
|
+
i := i + 1;
|
|
242
|
+
loop
|
|
243
|
+
if i > n then
|
|
244
|
+
raise exception
|
|
245
|
+
'agent_board_test.strip_sql: unterminated quoted identifier -- refusing to return a truncated body';
|
|
246
|
+
end if;
|
|
247
|
+
if c[i] = '"' then
|
|
248
|
+
if i < n and c[i + 1] = '"' then
|
|
249
|
+
i := i + 2; -- doubled quote inside the identifier
|
|
250
|
+
else
|
|
251
|
+
i := i + 1; exit;
|
|
252
|
+
end if;
|
|
253
|
+
else
|
|
254
|
+
i := i + 1;
|
|
255
|
+
end if;
|
|
256
|
+
end loop;
|
|
257
|
+
|
|
258
|
+
-- (4) single-quoted string literal, including the E'' escape form -------
|
|
259
|
+
elsif c[i] = '''' then
|
|
260
|
+
-- E'…' only when the E is a standalone prefix, not the tail of an
|
|
261
|
+
-- identifier (`v_note'…'` is not an escape string).
|
|
262
|
+
is_estring := i > 1
|
|
263
|
+
and upper(c[i - 1]) = 'E'
|
|
264
|
+
and (i = 2 or c[i - 2] !~ '[A-Za-z0-9_$]|[^\x00-\x7F]');
|
|
265
|
+
if not p_keep_literals then
|
|
266
|
+
-- an E prefix is part of the literal, not of the surrounding code
|
|
267
|
+
parts := parts || substr(p_text, span, (case when is_estring then i - 1 else i end) - span);
|
|
268
|
+
end if;
|
|
269
|
+
i := i + 1;
|
|
270
|
+
loop
|
|
271
|
+
if i > n then
|
|
272
|
+
raise exception
|
|
273
|
+
'agent_board_test.strip_sql: unterminated string literal -- refusing to return a truncated body';
|
|
274
|
+
end if;
|
|
275
|
+
if is_estring and c[i] = '\' then
|
|
276
|
+
i := i + 2; -- backslash escapes one char
|
|
277
|
+
elsif c[i] = '''' then
|
|
278
|
+
if i < n and c[i + 1] = '''' then
|
|
279
|
+
i := i + 2; -- doubled quote inside the literal
|
|
280
|
+
else
|
|
281
|
+
i := i + 1; exit;
|
|
282
|
+
end if;
|
|
283
|
+
else
|
|
284
|
+
i := i + 1;
|
|
285
|
+
end if;
|
|
286
|
+
end loop;
|
|
287
|
+
if not p_keep_literals then
|
|
288
|
+
span := i;
|
|
289
|
+
end if;
|
|
290
|
+
|
|
291
|
+
-- (5) dollar-quoted string -----------------------------------------------
|
|
292
|
+
--
|
|
293
|
+
-- TWO ways this is NOT a dollar quote, and both swallow live code if missed:
|
|
294
|
+
-- a. the tag cannot start with a digit -- `$1$` is the parameter $1
|
|
295
|
+
-- followed by $, not an opener;
|
|
296
|
+
-- b. PostgreSQL permits `$` INSIDE an unquoted identifier, and the lexer
|
|
297
|
+
-- consumes it greedily, so `foo$tag$danger$tag$` is ONE identifier.
|
|
298
|
+
-- A scanner that ignores the preceding character reads `$tag$` as an
|
|
299
|
+
-- opener and deletes `$tag$danger$tag$` from live code. Verified against
|
|
300
|
+
-- the server, not assumed: `do $do$ declare foo$tag$danger$tag$ int := 7;
|
|
301
|
+
-- begin raise notice '%', foo$tag$danger$tag$; end $do$;` runs and prints 7.
|
|
302
|
+
-- c. the identifier grammar is NOT ASCII. PostgreSQL accepts any letter
|
|
303
|
+
-- the server encoding recognises in an unquoted identifier AND in a
|
|
304
|
+
-- dollar-quote tag, so `[A-Za-z0-9_]` (or an \x80-\xff byte range, which
|
|
305
|
+
-- is wrong for multi-byte UTF-8) misreads both directions: `$Ω$ … $Ω$`
|
|
306
|
+
-- was left UNSTRIPPED -- a marker could hide in it -- and
|
|
307
|
+
-- `Ω$tag$danger$tag$` was eaten as if it opened a quote. Both verified
|
|
308
|
+
-- against the server: `do $do$ begin raise notice '%', $Ω$ hello $Ω$; end $do$;`
|
|
309
|
+
-- and `do $do$ declare Ω$tag$danger$tag$ int := 7; ... end $do$;` both run.
|
|
310
|
+
-- The class used here is LOCALE-INDEPENDENT on purpose. `[[:alpha:]]` is
|
|
311
|
+
-- not: under `C` collation it does NOT match `Ω` (measured:
|
|
312
|
+
-- `('Ω' collate "C") ~ '[[:alpha:]]'` is FALSE while the default collation
|
|
313
|
+
-- says true), so a locale-sensitive class makes this scanner -- and the
|
|
314
|
+
-- whole 49-site claim that rests on it -- pass or fail depending on the
|
|
315
|
+
-- runner's collation, and diverge from the JS mirror in scripts/.
|
|
316
|
+
-- For UTF-8 PostgreSQL treats ANY non-ASCII character as an identifier
|
|
317
|
+
-- character, so `[A-Za-z_]|[^\x00-\x7F]` is both exact and collation-free.
|
|
318
|
+
elsif c[i] = '$'
|
|
319
|
+
and (i = 1 or c[i - 1] !~ '[A-Za-z0-9_$]|[^\x00-\x7F]') then
|
|
320
|
+
j := i + 1;
|
|
321
|
+
if j <= n and c[j] ~ '[A-Za-z_]|[^\x00-\x7F]' then
|
|
322
|
+
while j <= n and c[j] ~ '[A-Za-z0-9_]|[^\x00-\x7F]' loop
|
|
323
|
+
j := j + 1;
|
|
324
|
+
end loop;
|
|
325
|
+
end if;
|
|
326
|
+
if j <= n and c[j] = '$' then
|
|
327
|
+
tag := substr(p_text, i, j - i + 1);
|
|
328
|
+
close_at := position(tag in substr(p_text, j + 1));
|
|
329
|
+
if close_at = 0 then
|
|
330
|
+
raise exception
|
|
331
|
+
'agent_board_test.strip_sql: unterminated dollar-quoted string % -- refusing to return a truncated body',
|
|
332
|
+
tag;
|
|
333
|
+
end if;
|
|
334
|
+
if not p_keep_literals then
|
|
335
|
+
parts := parts || substr(p_text, span, i - span);
|
|
336
|
+
end if;
|
|
337
|
+
i := j + close_at + length(tag); -- first char after the closing tag
|
|
338
|
+
if not p_keep_literals then
|
|
339
|
+
span := i;
|
|
340
|
+
end if;
|
|
341
|
+
else
|
|
342
|
+
i := i + 1; -- a bare `$` / `$1`: ordinary code
|
|
343
|
+
end if;
|
|
344
|
+
|
|
345
|
+
else
|
|
346
|
+
i := i + 1;
|
|
347
|
+
end if;
|
|
348
|
+
end loop;
|
|
349
|
+
|
|
350
|
+
parts := parts || substr(p_text, span);
|
|
351
|
+
-- Each stripped token collapses to a single space so token boundaries
|
|
352
|
+
-- survive: `a/*x*/b` must not become the identifier `ab`.
|
|
353
|
+
return array_to_string(parts, ' ');
|
|
354
|
+
end;
|
|
355
|
+
$strip$;
|
|
356
|
+
|
|
357
|
+
COMMENT ON FUNCTION agent_board_test.strip_sql(text, boolean) IS
|
|
358
|
+
'One-pass character scanner that removes line comments, NESTED block comments and (unless p_keep_literals) string literals from SQL/PL-pgSQL source text. Test-only; see message-board#196 / #199.';
|
|
359
|
+
|
|
360
|
+
-- The stripped BODY of a function. This is what a positional or negative
|
|
361
|
+
-- assertion should read.
|
|
362
|
+
CREATE OR REPLACE FUNCTION agent_board_test.stripped_body(
|
|
363
|
+
p_fn oid,
|
|
364
|
+
p_keep_literals boolean DEFAULT false
|
|
365
|
+
) RETURNS text
|
|
366
|
+
LANGUAGE sql
|
|
367
|
+
STABLE
|
|
368
|
+
SET search_path = pg_catalog
|
|
369
|
+
AS $$
|
|
370
|
+
SELECT agent_board_test.strip_sql(p.prosrc, p_keep_literals)
|
|
371
|
+
FROM pg_proc p
|
|
372
|
+
WHERE p.oid = p_fn;
|
|
373
|
+
$$;
|
|
374
|
+
|
|
375
|
+
-- The stripped FULL DEFINITION. The header (`SECURITY DEFINER`, `SET
|
|
376
|
+
-- search_path = …`, the argument list) is regenerated by Postgres from catalog
|
|
377
|
+
-- metadata and cannot carry author prose, so it is left intact -- assertions
|
|
378
|
+
-- that match on it keep working. Only the body is stripped.
|
|
379
|
+
--
|
|
380
|
+
-- Stripping pg_get_functiondef() wholesale would be a self-inflicted wound:
|
|
381
|
+
-- the body is wrapped in a dollar quote (`AS $function$ … $function$`), so a
|
|
382
|
+
-- dollar-quote-aware strip would delete THE ENTIRE BODY and every `!~` over the
|
|
383
|
+
-- result would pass. Splicing avoids that trap by construction.
|
|
384
|
+
CREATE OR REPLACE FUNCTION agent_board_test.stripped_def(
|
|
385
|
+
p_fn oid,
|
|
386
|
+
p_keep_literals boolean DEFAULT false
|
|
387
|
+
) RETURNS text
|
|
388
|
+
LANGUAGE plpgsql
|
|
389
|
+
STABLE
|
|
390
|
+
SET search_path = pg_catalog
|
|
391
|
+
AS $$
|
|
392
|
+
declare
|
|
393
|
+
v_def text;
|
|
394
|
+
v_src text;
|
|
395
|
+
v_at int;
|
|
396
|
+
begin
|
|
397
|
+
select pg_get_functiondef(p.oid), p.prosrc into v_def, v_src
|
|
398
|
+
from pg_proc p where p.oid = p_fn;
|
|
399
|
+
if v_def is null then
|
|
400
|
+
return null;
|
|
401
|
+
end if;
|
|
402
|
+
if v_src is null or v_src = '' then
|
|
403
|
+
return v_def;
|
|
404
|
+
end if;
|
|
405
|
+
v_at := position(v_src in v_def);
|
|
406
|
+
if v_at = 0 then
|
|
407
|
+
raise exception
|
|
408
|
+
'agent_board_test.stripped_def: prosrc not found verbatim inside pg_get_functiondef for oid % -- refusing to guess', p_fn;
|
|
409
|
+
end if;
|
|
410
|
+
return substr(v_def, 1, v_at - 1)
|
|
411
|
+
|| agent_board_test.strip_sql(v_src, p_keep_literals)
|
|
412
|
+
|| substr(v_def, v_at + length(v_src));
|
|
413
|
+
end;
|
|
414
|
+
$$;
|
|
415
|
+
|
|
416
|
+
-- View definitions are REGENERATED by Postgres, so comments are already gone --
|
|
417
|
+
-- but string literals in the select list are not, and a negative assertion over
|
|
418
|
+
-- a view body is maskable by one.
|
|
419
|
+
CREATE OR REPLACE FUNCTION agent_board_test.stripped_viewdef(
|
|
420
|
+
p_rel oid,
|
|
421
|
+
p_keep_literals boolean DEFAULT false
|
|
422
|
+
) RETURNS text
|
|
423
|
+
LANGUAGE sql
|
|
424
|
+
STABLE
|
|
425
|
+
SET search_path = pg_catalog
|
|
426
|
+
AS $$
|
|
427
|
+
SELECT agent_board_test.strip_sql(pg_get_viewdef(p_rel), p_keep_literals);
|
|
428
|
+
$$;
|
|
429
|
+
|
|
430
|
+
GRANT USAGE ON SCHEMA agent_board_test TO PUBLIC;
|
|
@@ -864,7 +864,7 @@ step retry_onboard:
|
|
|
864
864
|
<waiting ...>
|
|
865
865
|
step lifecycle_commit: COMMIT;
|
|
866
866
|
step retry_onboard: <... completed>
|
|
867
|
-
ERROR:
|
|
867
|
+
ERROR: this signing key is already enrolled to you as agent "atomic-lifecycle-agent", but that identity is not resumable (its key or agent is inactive or unproven, its stored public key differs, or its enrollment audit records no granted scopes); enrollment need not be repeated; report this handle to an operator
|
|
868
868
|
step retry_commit: COMMIT;
|
|
869
869
|
step check:
|
|
870
870
|
select a.status as agent_status,
|
|
@@ -929,7 +929,7 @@ step retry_onboard:
|
|
|
929
929
|
<waiting ...>
|
|
930
930
|
step lifecycle_commit: COMMIT;
|
|
931
931
|
step retry_onboard: <... completed>
|
|
932
|
-
ERROR:
|
|
932
|
+
ERROR: this signing key is already enrolled to you as agent "atomic-lifecycle-agent", but that identity is not resumable (its key or agent is inactive or unproven, its stored public key differs, or its enrollment audit records no granted scopes); enrollment need not be repeated; report this handle to an operator
|
|
933
933
|
step retry_commit: COMMIT;
|
|
934
934
|
step check:
|
|
935
935
|
select a.status as agent_status,
|
|
@@ -994,7 +994,7 @@ step retry_onboard:
|
|
|
994
994
|
<waiting ...>
|
|
995
995
|
step lifecycle_commit: COMMIT;
|
|
996
996
|
step retry_onboard: <... completed>
|
|
997
|
-
ERROR:
|
|
997
|
+
ERROR: this signing key is already enrolled to you as agent "atomic-lifecycle-agent", but that identity is not resumable (its key or agent is inactive or unproven, its stored public key differs, or its enrollment audit records no granted scopes); enrollment need not be repeated; report this handle to an operator
|
|
998
998
|
step retry_commit: COMMIT;
|
|
999
999
|
step check:
|
|
1000
1000
|
select a.status as agent_status,
|