session-orchestrator 3.19.0 → 3.20.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +80 -0
- package/README.md +9 -9
- package/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +1 -1
- package/docs/instruction-delivery.md +350 -0
- package/docs/session-config-reference.md +1 -41
- package/docs/session-config-template.md +0 -23
- package/hooks/_lib/guard-source-loader.mjs +304 -91
- package/hooks/enforce-commands.mjs +216 -17
- package/hooks/enforce-scope.mjs +133 -9
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-start.mjs +7 -4
- package/hooks/pre-bash-destructive-guard.mjs +146 -59
- package/hooks/pre-bash-sessions-ledger-guard.mjs +493 -66
- package/package.json +2 -2
- package/scripts/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/emit-session.mjs +3 -40
- package/scripts/lib/command-blocker.mjs +322 -62
- package/scripts/lib/hardening.mjs +9 -9
- package/scripts/lib/learnings/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/select.mjs +550 -0
- package/scripts/lib/reconcile/emitter.mjs +107 -22
- package/scripts/lib/reconcile/engine.mjs +9 -15
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +95 -1
- package/scripts/lib/scope-gate.mjs +194 -72
- package/scripts/lib/session-close-backfill.mjs +2 -2
- package/scripts/lib/session-record-repair.mjs +551 -0
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- package/scripts/lib/soul-resolve.mjs +12 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +3 -3
- package/scripts/lib/validate/check-rules.mjs +31 -5
- package/scripts/lib/validate/check-unwired-features.mjs +549 -0
- package/scripts/print-applicable-rules.mjs +170 -7
- package/scripts/print-learnings-index.mjs +474 -0
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +21 -0
- package/skills/brainstorm/soul.md +47 -1
- package/skills/evolve/SKILL.md +116 -18
- package/skills/gitlab-ops/SKILL.md +5 -0
- package/skills/grill/soul.md +44 -1
- package/skills/plan/soul.md +46 -3
- package/skills/session-end/SKILL.md +1 -24
- package/skills/session-end/phase-3-6-tail.md +30 -1
- package/skills/session-end/plan-verification.md +1 -5
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-start/SKILL.md +2 -0
- package/skills/session-start/soul.md +41 -1
- package/skills/wave-executor/SKILL.md +1 -5
- package/skills/wave-executor/wave-loop.md +36 -71
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
17
|
-
import { tokenizeCommand } from './command-blocker.mjs';
|
|
17
|
+
import { tokenizeCommand, splitChainSegments, resolveSegmentVerb } from './command-blocker.mjs';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Find the wave-scope.json file for the given project root.
|
|
@@ -573,6 +573,25 @@ export function assertTestSiblingCoverage(fileScope, allowedPaths, opts = {}) {
|
|
|
573
573
|
* emits no token at all — an improvement: `tee out.txt <<EOF` used to report
|
|
574
574
|
* the literal `EOF` as a second write target.)
|
|
575
575
|
*
|
|
576
|
+
* ## Segment-first, wrapper-aware verb resolution (#996.2)
|
|
577
|
+
*
|
|
578
|
+
* The interpretation is SEGMENT-first: the token stream is split on shell chain
|
|
579
|
+
* operators / newlines by {@link splitChainSegments} BEFORE it is interpreted,
|
|
580
|
+
* and each segment's real command verb is resolved through the shared
|
|
581
|
+
* {@link resolveSegmentVerb} wrapper table. A lone `expectCommand` flag used to
|
|
582
|
+
* read the FIRST word of a segment as the command head, so a transparent wrapper
|
|
583
|
+
* (`sudo`, `env FOO=1`, `timeout 5`, `nice -n 10`, `command`, `/usr/bin/time`)
|
|
584
|
+
* consumed the head slot and the REAL verb (`tee`/`sed`/`dd`) landed in argument
|
|
585
|
+
* position where it was never matched — measured `sudo tee src/x.ts` → `[]`, a
|
|
586
|
+
* DETECTION LOSS. This is the same fail-open class #991 closed in the ledger
|
|
587
|
+
* guard, in a warn-only (#800) consumer. `resolveSegmentVerb` unwraps the
|
|
588
|
+
* wrappers and returns a BASENAME-normalized verb, so an absolute `/usr/bin/tee`
|
|
589
|
+
* resolves to `tee` for free. It ALSO reports wrapper-written file operands
|
|
590
|
+
* (`writesFile: true`, the #992 discriminator) — `/usr/bin/time -o FILE npm test`
|
|
591
|
+
* truncates FILE while the verb is `npm`, a write with no redirect operator and
|
|
592
|
+
* no tee/sed/dd head; those are harvested exactly as {@link extractRedirectTargets}
|
|
593
|
+
* does.
|
|
594
|
+
*
|
|
576
595
|
* Hook-safe: pure, deterministic, no I/O. Never throws — a non-string / empty
|
|
577
596
|
* input returns `[]`.
|
|
578
597
|
*
|
|
@@ -582,8 +601,6 @@ export function assertTestSiblingCoverage(fileScope, allowedPaths, opts = {}) {
|
|
|
582
601
|
export function extractBashWriteTargets(command) {
|
|
583
602
|
if (typeof command !== 'string' || command.length === 0) return [];
|
|
584
603
|
|
|
585
|
-
const tokens = classifyShellTokens(tokenizeCommand(command));
|
|
586
|
-
|
|
587
604
|
const out = [];
|
|
588
605
|
const seen = new Set();
|
|
589
606
|
const add = (value) => {
|
|
@@ -594,21 +611,104 @@ export function extractBashWriteTargets(command) {
|
|
|
594
611
|
out.push(v);
|
|
595
612
|
};
|
|
596
613
|
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
614
|
+
// Segment-first (#996.2): split on shell chain operators / newlines BEFORE
|
|
615
|
+
// interpreting, then resolve each segment's REAL verb through the shared
|
|
616
|
+
// wrapper table. `out`/`seen`/`add` stay outside the loop so de-duplication
|
|
617
|
+
// and first-seen ordering hold across segments (`echo a > x; echo b >> x`).
|
|
618
|
+
for (const segment of splitChainSegments(tokenizeCommand(command))) {
|
|
619
|
+
// #996.2 namespace fix: resolve the verb in the SAME (paren-peeled,
|
|
620
|
+
// redirect-target-excluded) namespace the interpretation loop's `wordsSeen`
|
|
621
|
+
// counter walks below — see {@link verbResolutionSegment}. Passing the RAW
|
|
622
|
+
// segment mis-resolved a LEADING redirect (`> a.txt tee b.ts` → verb `>`) or a
|
|
623
|
+
// LEADING subshell paren (`(tee inner.ts)` → verb `(tee`): mode never became
|
|
624
|
+
// `tee`/`sed`/`dd` and the `index + 1` head-skip then swallowed the real
|
|
625
|
+
// write target. For A5's 10 wrapper forms (no leading redirect/paren) the view
|
|
626
|
+
// is a no-op, so verb/index/wrapperArgs — incl. the #992 `writesFile` marks —
|
|
627
|
+
// are byte-identical to the raw call.
|
|
628
|
+
const { verb, index, wrapperArgs } = resolveSegmentVerb(verbResolutionSegment(segment));
|
|
629
|
+
const tokens = classifyShellTokens(segment);
|
|
630
|
+
|
|
631
|
+
// A wrapper can WRITE a file through its OWN option, with no redirect and no
|
|
632
|
+
// tee/sed/dd verb: `/usr/bin/time -o src/report.txt npm test` truncates
|
|
633
|
+
// src/report.txt while the resolved verb is `npm`. `resolveSegmentVerb` marks
|
|
634
|
+
// exactly those operands `writesFile: true` (the #992 discriminator that
|
|
635
|
+
// keeps `nice -n 10` / `sudo -u root` operands OUT). Harvest them exactly as
|
|
636
|
+
// collectRedirectTargets does — the write is otherwise invisible to this pass.
|
|
637
|
+
for (const wa of wrapperArgs) {
|
|
638
|
+
if (wa.writesFile === true && typeof wa.value === 'string') add(wa.value);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// `mode` tracks a command-head that owns following args (tee/sed/dd). It is
|
|
642
|
+
// set ONCE per segment from the resolved verb, which `resolveSegmentVerb`
|
|
643
|
+
// returns basename-normalized — so an absolute `/usr/bin/tee` resolves to
|
|
644
|
+
// `tee` for free. `pendingRedirect` marks that the NEXT word is a redirect
|
|
645
|
+
// target; `heredocOpen` that a here-doc BODY word (DATA) follows.
|
|
646
|
+
const mode = verb === 'tee' || verb === 'sed' || verb === 'dd' ? verb : null;
|
|
647
|
+
let pendingRedirect = false;
|
|
648
|
+
let heredocOpen = false;
|
|
649
|
+
const sedArgs = []; // classified word tokens collected for a `sed -i` head
|
|
650
|
+
let sedInPlace = false;
|
|
651
|
+
|
|
652
|
+
// THE ONE CARE POINT of the #996.2 refactor: the first `index + 1` WORD
|
|
653
|
+
// tokens that REACH the `wordsSeen` counter are the wrapper chain + the verb
|
|
654
|
+
// itself, never file arguments. `index` now comes from
|
|
655
|
+
// {@link verbResolutionSegment}, which resolves the verb in the SAME namespace
|
|
656
|
+
// this loop walks — paren-peeled words with redirect targets removed — so
|
|
657
|
+
// `index` is 1:1 with the `word` tokens that reach the counter below (a
|
|
658
|
+
// leading redirect or subshell paren no longer shifts it out of alignment).
|
|
659
|
+
// `peelSubshellParens` emits AT MOST one `word` per raw token.
|
|
660
|
+
const headWordsToSkip = index + 1;
|
|
661
|
+
let wordsSeen = 0;
|
|
662
|
+
|
|
663
|
+
for (const tk of tokens) {
|
|
664
|
+
if (tk.type === 'redirect') {
|
|
665
|
+
pendingRedirect = true;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (tk.type === 'heredoc') {
|
|
669
|
+
// here-doc: the delimiter emits no token; the BODY arrives later as data
|
|
670
|
+
pendingRedirect = false;
|
|
671
|
+
heredocOpen = true;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (tk.type === 'in') {
|
|
675
|
+
// input redirect / here-string — not a write target
|
|
676
|
+
pendingRedirect = false;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (tk.type === 'sep') {
|
|
680
|
+
// Within a segment the ONLY seps are peeled subshell parens — chain
|
|
681
|
+
// operators were already consumed by splitChainSegments. Reset only the
|
|
682
|
+
// pending redirect so `> >(cat)` stays inert (without it the peeled `cat`
|
|
683
|
+
// reads as the redirect target); mode/heredoc reset live at segment end.
|
|
684
|
+
pendingRedirect = false;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
// word token
|
|
688
|
+
if (pendingRedirect) {
|
|
689
|
+
add(tk.value);
|
|
690
|
+
pendingRedirect = false;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (wordsSeen < headWordsToSkip) {
|
|
694
|
+
wordsSeen++; // wrapper chain + verb head — not a file argument
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
// subsequent argument words, interpreted per active command-head mode
|
|
698
|
+
if (heredocOpen) continue; // DATA, not a file argument
|
|
699
|
+
if (mode === 'tee') {
|
|
700
|
+
if (!isShellFlag(tk.value)) add(tk.value);
|
|
701
|
+
} else if (mode === 'sed') {
|
|
702
|
+
if (/^-i/.test(tk.value)) sedInPlace = true;
|
|
703
|
+
sedArgs.push(tk);
|
|
704
|
+
} else if (mode === 'dd') {
|
|
705
|
+
if (tk.value.startsWith('of=')) add(tk.value.slice(3));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Segment end: flush a pending `sed -i` file argument (last non-flag arg).
|
|
710
|
+
// Only reached once per segment now that each segment carries a single verb,
|
|
711
|
+
// so the former sep-branch / defensive flushes are unnecessary.
|
|
612
712
|
if (mode === 'sed' && sedInPlace) {
|
|
613
713
|
for (let i = sedArgs.length - 1; i >= 0; i--) {
|
|
614
714
|
if (!isShellFlag(sedArgs[i].value)) {
|
|
@@ -617,61 +717,7 @@ export function extractBashWriteTargets(command) {
|
|
|
617
717
|
}
|
|
618
718
|
}
|
|
619
719
|
}
|
|
620
|
-
sedArgs = [];
|
|
621
|
-
sedInPlace = false;
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
for (const tk of tokens) {
|
|
625
|
-
if (tk.type === 'redirect') {
|
|
626
|
-
pendingRedirect = true;
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
if (tk.type === 'heredoc') {
|
|
630
|
-
// here-doc: the delimiter emits no token; the BODY arrives later as data
|
|
631
|
-
pendingRedirect = false;
|
|
632
|
-
heredocOpen = true;
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
if (tk.type === 'in') {
|
|
636
|
-
// input redirect / here-string — not a write target
|
|
637
|
-
pendingRedirect = false;
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
if (tk.type === 'sep') {
|
|
641
|
-
flushSed();
|
|
642
|
-
mode = null;
|
|
643
|
-
pendingRedirect = false;
|
|
644
|
-
heredocOpen = false;
|
|
645
|
-
expectCommand = true;
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
// word token
|
|
649
|
-
if (pendingRedirect) {
|
|
650
|
-
add(tk.value);
|
|
651
|
-
pendingRedirect = false;
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
if (expectCommand) {
|
|
655
|
-
expectCommand = false;
|
|
656
|
-
flushSed(); // flush any prior sed segment defensively
|
|
657
|
-
if (tk.value === 'tee') { mode = 'tee'; continue; }
|
|
658
|
-
if (tk.value === 'sed') { mode = 'sed'; continue; }
|
|
659
|
-
if (tk.value === 'dd') { mode = 'dd'; continue; }
|
|
660
|
-
mode = null;
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
// subsequent argument words, interpreted per active command-head mode
|
|
664
|
-
if (heredocOpen) continue; // DATA, not a file argument
|
|
665
|
-
if (mode === 'tee') {
|
|
666
|
-
if (!isShellFlag(tk.value)) add(tk.value);
|
|
667
|
-
} else if (mode === 'sed') {
|
|
668
|
-
if (/^-i/.test(tk.value)) sedInPlace = true;
|
|
669
|
-
sedArgs.push(tk);
|
|
670
|
-
} else if (mode === 'dd') {
|
|
671
|
-
if (tk.value.startsWith('of=')) add(tk.value.slice(3));
|
|
672
|
-
}
|
|
673
720
|
}
|
|
674
|
-
flushSed();
|
|
675
721
|
|
|
676
722
|
return out;
|
|
677
723
|
}
|
|
@@ -776,6 +822,82 @@ function classifyShellTokens(tokens) {
|
|
|
776
822
|
return out;
|
|
777
823
|
}
|
|
778
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Project a raw chain segment into the token list used for VERB resolution,
|
|
827
|
+
* in the SAME namespace {@link extractBashWriteTargets}'s interpretation loop
|
|
828
|
+
* walks with its `wordsSeen` counter (#996.2 namespace fix).
|
|
829
|
+
*
|
|
830
|
+
* {@link resolveSegmentVerb} resolves argv[0] from RAW tokens. Two segment shapes
|
|
831
|
+
* put a NON-verb token in raw position 0 and so mis-resolve the command head,
|
|
832
|
+
* losing the write target the pre-#996.2 pass had detected:
|
|
833
|
+
* - a LEADING write-redirect — `> a.txt tee b.ts` resolves the `>` operator as
|
|
834
|
+
* the "verb" (mode never becomes `tee`), and the loop's `index + 1` head-skip
|
|
835
|
+
* then swallows `tee`, dropping `b.ts`.
|
|
836
|
+
* - a LEADING subshell paren — `(tee inner.ts)` keeps the raw token `(tee`
|
|
837
|
+
* (`.replace(/^.*\//,'')` leaves it untouched, no `/`), so the wrapper table
|
|
838
|
+
* never matches: verb is the literal `(tee`, again mode-less. The classified
|
|
839
|
+
* stream peels the paren AFTER, so the raw verb index and the peeled word
|
|
840
|
+
* stream diverge.
|
|
841
|
+
*
|
|
842
|
+
* The projection makes both namespaces agree: peel leading/trailing parens off
|
|
843
|
+
* each unquoted token (matching {@link peelSubshellParens}' WORD output) and drop
|
|
844
|
+
* every write-redirect operator together with its target word — exactly the
|
|
845
|
+
* tokens the interpretation loop keeps OUT of `wordsSeen` (redirect operators are
|
|
846
|
+
* not `word`s; their targets are consumed by the `pendingRedirect` branch before
|
|
847
|
+
* the counter). The resulting segment's verb INDEX is then 1:1 with `wordsSeen`,
|
|
848
|
+
* so `index + 1` skips the wrapper chain + verb and no more.
|
|
849
|
+
*
|
|
850
|
+
* A5's 10 transparent-wrapper forms carry neither a leading redirect nor a paren,
|
|
851
|
+
* so this is a NO-OP for them: `resolveSegmentVerb`'s verb/index/wrapperArgs (incl.
|
|
852
|
+
* the #992 `writesFile` operand marks) are byte-identical to the raw-segment call.
|
|
853
|
+
*
|
|
854
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
855
|
+
* @returns {Array<{ text: string, quoted: boolean }>}
|
|
856
|
+
*/
|
|
857
|
+
function verbResolutionSegment(segment) {
|
|
858
|
+
const out = [];
|
|
859
|
+
let pendingRedirectTarget = false;
|
|
860
|
+
for (const tok of segment) {
|
|
861
|
+
if (!tok.quoted) {
|
|
862
|
+
// A write redirect consumes the NEXT token as its target — neither the
|
|
863
|
+
// operator nor its target reaches `wordsSeen`, so both must be absent here
|
|
864
|
+
// to keep the verb index aligned.
|
|
865
|
+
if (WRITE_REDIRECT_OP_RE.test(tok.text)) {
|
|
866
|
+
pendingRedirectTarget = true;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
// heredoc / input-redirect operators carry no immediate target token that
|
|
870
|
+
// reaches `wordsSeen` (the here-doc delimiter emits nothing; an input target
|
|
871
|
+
// DOES reach the counter and so is kept) — drop only the operator itself.
|
|
872
|
+
if (HEREDOC_OP_RE.test(tok.text) || INPUT_REDIRECT_OP_RE.test(tok.text)) {
|
|
873
|
+
pendingRedirectTarget = false;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
// Within a segment the only separator that can appear is an fd-dup `&`
|
|
877
|
+
// (e.g. `2>&1`), which classifyShellTokens also treats as a `sep` that
|
|
878
|
+
// resets the pending redirect — mirror that so the target after it stays.
|
|
879
|
+
if (SHELL_SEPARATOR_OPS.has(tok.text)) {
|
|
880
|
+
pendingRedirectTarget = false;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (pendingRedirectTarget) {
|
|
885
|
+
pendingRedirectTarget = false;
|
|
886
|
+
continue; // this token is the dropped write-redirect target
|
|
887
|
+
}
|
|
888
|
+
if (tok.quoted) {
|
|
889
|
+
out.push(tok);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
let text = tok.text;
|
|
893
|
+
while (text.startsWith('(')) text = text.slice(1);
|
|
894
|
+
while (text.endsWith(')')) text = text.slice(0, -1);
|
|
895
|
+
if (text.length === 0) continue; // pure paren = a separator, not a word
|
|
896
|
+
out.push({ text, quoted: false });
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
|
|
779
901
|
/**
|
|
780
902
|
* Build an actionable suggestion string for a scope violation.
|
|
781
903
|
*
|
|
@@ -42,7 +42,7 @@ import crypto from 'node:crypto';
|
|
|
42
42
|
import { appendJsonl as defaultAppendJsonl } from './common.mjs';
|
|
43
43
|
import { readLock as defaultReadLock, isLockLive as defaultIsLockLive, DEFAULT_TTL_HOURS } from './session-lock.mjs';
|
|
44
44
|
import { validateSession as defaultValidateSession } from './session-schema/validator.mjs';
|
|
45
|
-
import { serializeSessionLineChecked as defaultSerialize } from '
|
|
45
|
+
import { serializeSessionLineChecked as defaultSerialize } from './session-schema.mjs';
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
// Constants
|
|
@@ -512,7 +512,7 @@ export async function backfillAbandonedSession({
|
|
|
512
512
|
let validated;
|
|
513
513
|
try {
|
|
514
514
|
validated = validateSession(record);
|
|
515
|
-
serializeSessionLineChecked(
|
|
515
|
+
serializeSessionLineChecked(record);
|
|
516
516
|
} catch (err) {
|
|
517
517
|
return { action: 'error', error: `validation: ${err?.message ?? String(err)}`, sessionId: recordId };
|
|
518
518
|
}
|