sentinelayer-cli 0.34.3 → 0.34.5
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/package.json +1 -1
- package/src/commands/session.js +25 -9
- package/src/legacy-cli.js +26 -5
- package/src/review/local-review.js +14 -0
- package/src/scan/generator.js +26 -5
- package/src/session/control-events.js +17 -0
- package/src/session/sync.js +6 -1
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -783,9 +783,14 @@ function mergeSessionActionEvents(events = [], actionEvents = []) {
|
|
|
783
783
|
return dedupeSessionEvents([...(Array.isArray(events) ? events : []), ...actionEvents])
|
|
784
784
|
.map((event, index) => ({ event, index }))
|
|
785
785
|
.sort((left, right) => {
|
|
786
|
+
const leftSequence = eventSequenceNumber(left.event);
|
|
787
|
+
const rightSequence = eventSequenceNumber(right.event);
|
|
788
|
+
if (leftSequence > 0 && rightSequence > 0 && leftSequence !== rightSequence) {
|
|
789
|
+
return leftSequence - rightSequence;
|
|
790
|
+
}
|
|
786
791
|
const timeDiff = eventTimestampMs(left.event) - eventTimestampMs(right.event);
|
|
787
792
|
if (timeDiff !== 0) return timeDiff;
|
|
788
|
-
const sequenceDiff =
|
|
793
|
+
const sequenceDiff = leftSequence - rightSequence;
|
|
789
794
|
if (sequenceDiff !== 0) return sequenceDiff;
|
|
790
795
|
return left.index - right.index;
|
|
791
796
|
})
|
|
@@ -5574,6 +5579,9 @@ export function registerSessionCommand(program) {
|
|
|
5574
5579
|
const rawActionEvents = remoteActions?.ok
|
|
5575
5580
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
5576
5581
|
: [];
|
|
5582
|
+
const visibleActionEvents = includeControlEvents
|
|
5583
|
+
? rawActionEvents
|
|
5584
|
+
: rawActionEvents.filter((event) => !isSessionControlEvent(event));
|
|
5577
5585
|
const unacknowledgedHumanMessages = remoteActions?.ok
|
|
5578
5586
|
? readProjectionUnacknowledgedHumanMessages(remoteActions)
|
|
5579
5587
|
: [];
|
|
@@ -5593,8 +5601,8 @@ export function registerSessionCommand(program) {
|
|
|
5593
5601
|
: sourceEvents.filter((event) => !isSessionControlEvent(event));
|
|
5594
5602
|
const hiddenControlEventCount = sourceEvents.length - transcriptEvents.length;
|
|
5595
5603
|
const actionEvents = remoteWindowRead
|
|
5596
|
-
? filterSessionActionEventsForTranscriptWindow(
|
|
5597
|
-
:
|
|
5604
|
+
? filterSessionActionEventsForTranscriptWindow(visibleActionEvents, transcriptEvents)
|
|
5605
|
+
: visibleActionEvents;
|
|
5598
5606
|
const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
|
|
5599
5607
|
const autoView = recordSessionReadViews(normalizedSessionId, events, {
|
|
5600
5608
|
targetPath,
|
|
@@ -6245,15 +6253,19 @@ export function registerSessionCommand(program) {
|
|
|
6245
6253
|
limit: 5_000,
|
|
6246
6254
|
}),
|
|
6247
6255
|
]);
|
|
6248
|
-
const
|
|
6256
|
+
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
6257
|
+
const rawActionEvents = remoteActions?.ok
|
|
6249
6258
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
6250
6259
|
: [];
|
|
6251
|
-
const
|
|
6260
|
+
const actionEvents = includeControlEvents
|
|
6261
|
+
? rawActionEvents
|
|
6262
|
+
: rawActionEvents.filter((event) => !isSessionControlEvent(event));
|
|
6252
6263
|
const dedupedEvents = dedupeSessionEvents(events);
|
|
6253
6264
|
const transcriptBaseEvents = includeControlEvents
|
|
6254
6265
|
? dedupedEvents
|
|
6255
6266
|
: filterSessionMaterialEvents(dedupedEvents);
|
|
6256
|
-
const hiddenControlEventCount =
|
|
6267
|
+
const hiddenControlEventCount =
|
|
6268
|
+
dedupedEvents.length - transcriptBaseEvents.length + rawActionEvents.length - actionEvents.length;
|
|
6257
6269
|
const exportEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
6258
6270
|
const stats = computeTranscriptStats({
|
|
6259
6271
|
sessionMeta: sessionPayload,
|
|
@@ -6400,15 +6412,19 @@ export function registerSessionCommand(program) {
|
|
|
6400
6412
|
listAgents(normalizedSessionId, { targetPath, includeInactive: true }),
|
|
6401
6413
|
readStream(normalizedSessionId, { targetPath, tail: 0 }),
|
|
6402
6414
|
]);
|
|
6403
|
-
const
|
|
6415
|
+
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
6416
|
+
const rawActionEvents = remoteActions?.ok
|
|
6404
6417
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
6405
6418
|
: [];
|
|
6406
|
-
const
|
|
6419
|
+
const actionEvents = includeControlEvents
|
|
6420
|
+
? rawActionEvents
|
|
6421
|
+
: rawActionEvents.filter((event) => !isSessionControlEvent(event));
|
|
6407
6422
|
const dedupedEvents = dedupeSessionEvents(events);
|
|
6408
6423
|
const transcriptBaseEvents = includeControlEvents
|
|
6409
6424
|
? dedupedEvents
|
|
6410
6425
|
: filterSessionMaterialEvents(dedupedEvents);
|
|
6411
|
-
const hiddenControlEventCount =
|
|
6426
|
+
const hiddenControlEventCount =
|
|
6427
|
+
dedupedEvents.length - transcriptBaseEvents.length + rawActionEvents.length - actionEvents.length;
|
|
6412
6428
|
const transcriptEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
6413
6429
|
|
|
6414
6430
|
// Pull GitHub/Google avatar + display name from the active auth
|
package/src/legacy-cli.js
CHANGED
|
@@ -2520,16 +2520,22 @@ jobs:
|
|
|
2520
2520
|
uses: mrrCarter/sentinelayer-v1-action@03d7369cba7de2e9f15b959275c982111f0ee493
|
|
2521
2521
|
with:
|
|
2522
2522
|
github_token: \${{ github.token }}
|
|
2523
|
+
openai_api_key: \${{ secrets.OPENAI_API_KEY }}
|
|
2524
|
+
google_api_key: \${{ secrets.GOOGLE_API_KEY }}
|
|
2525
|
+
llm_provider: \${{ secrets.GOOGLE_API_KEY != '' && 'google' || 'openai' }}
|
|
2523
2526
|
sentinelayer_token: \${{ secrets.${normalizedSecret} }}${specIdBindingLine}
|
|
2524
|
-
sentinelayer_managed_llm:
|
|
2527
|
+
sentinelayer_managed_llm: \${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}
|
|
2525
2528
|
scan_mode: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
|
|
2526
2529
|
severity_gate: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
|
|
2527
|
-
model: gpt-5.3-codex
|
|
2530
|
+
model: \${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-pro' || 'gpt-5.3-codex' }}
|
|
2528
2531
|
codex_model: gpt-5.3-codex
|
|
2529
|
-
model_fallback: gpt-4.1-mini
|
|
2530
|
-
use_codex:
|
|
2532
|
+
model_fallback: \${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}
|
|
2533
|
+
use_codex: \${{ secrets.GOOGLE_API_KEY == '' }}
|
|
2531
2534
|
codex_only: "false"
|
|
2532
2535
|
llm_failure_policy: block
|
|
2536
|
+
max_daily_scans: \${{ vars.OMAR_MAX_DAILY_SCANS || '200' }}
|
|
2537
|
+
min_scan_interval_minutes: \${{ vars.OMAR_MIN_SCAN_INTERVAL_MINUTES || '0' }}
|
|
2538
|
+
rate_limit_fail_mode: closed
|
|
2533
2539
|
- name: Enforce Omar reviewer merge thresholds
|
|
2534
2540
|
shell: bash
|
|
2535
2541
|
env:
|
|
@@ -2575,6 +2581,12 @@ jobs:
|
|
|
2575
2581
|
OMAR_SCAN_MODE: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
|
|
2576
2582
|
OMAR_SEVERITY_GATE: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
|
|
2577
2583
|
OMAR_P2_MAX_ALLOWED: \${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}
|
|
2584
|
+
OMAR_OPENAI_KEY_PRESENT: \${{ secrets.OPENAI_API_KEY != '' }}
|
|
2585
|
+
OMAR_GOOGLE_KEY_PRESENT: \${{ secrets.GOOGLE_API_KEY != '' }}
|
|
2586
|
+
OMAR_MANAGED_LLM: \${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}
|
|
2587
|
+
OMAR_LLM_PROVIDER: \${{ secrets.GOOGLE_API_KEY != '' && 'google' || 'openai' }}
|
|
2588
|
+
OMAR_MODEL: \${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-pro' || 'gpt-5.3-codex' }}
|
|
2589
|
+
OMAR_MODEL_FALLBACK: \${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}
|
|
2578
2590
|
run: |
|
|
2579
2591
|
set -euo pipefail
|
|
2580
2592
|
mkdir -p omar-artifacts
|
|
@@ -2590,6 +2602,9 @@ jobs:
|
|
|
2590
2602
|
value = env(name, "0").strip()
|
|
2591
2603
|
return int(value) if value.isdigit() else 0
|
|
2592
2604
|
|
|
2605
|
+
def bool_env(name):
|
|
2606
|
+
return env(name).strip().lower() == "true"
|
|
2607
|
+
|
|
2593
2608
|
github_run_id = env("GITHUB_RUN_ID")
|
|
2594
2609
|
server_url = env("GITHUB_SERVER_URL", "https://github.com").rstrip("/")
|
|
2595
2610
|
repository = env("GITHUB_REPOSITORY")
|
|
@@ -2612,7 +2627,13 @@ jobs:
|
|
|
2612
2627
|
"scan": {
|
|
2613
2628
|
"mode": env("OMAR_SCAN_MODE", "deep"),
|
|
2614
2629
|
"action_ref": "mrrCarter/sentinelayer-v1-action@03d7369cba7de2e9f15b959275c982111f0ee493",
|
|
2615
|
-
"
|
|
2630
|
+
"llm_provider": env("OMAR_LLM_PROVIDER", "openai"),
|
|
2631
|
+
"model": env("OMAR_MODEL", "gpt-5.3-codex"),
|
|
2632
|
+
"model_fallback": env("OMAR_MODEL_FALLBACK", "gpt-4.1-mini"),
|
|
2633
|
+
"llm_route": "google_api_key" if bool_env("OMAR_GOOGLE_KEY_PRESENT") else ("openai_api_key" if bool_env("OMAR_OPENAI_KEY_PRESENT") else "sentinelayer_managed"),
|
|
2634
|
+
"google_key_present": bool_env("OMAR_GOOGLE_KEY_PRESENT"),
|
|
2635
|
+
"openai_key_present": bool_env("OMAR_OPENAI_KEY_PRESENT"),
|
|
2636
|
+
"managed_llm": bool_env("OMAR_MANAGED_LLM"),
|
|
2616
2637
|
"llm_failure_policy": "block",
|
|
2617
2638
|
},
|
|
2618
2639
|
"github": {
|
|
@@ -95,6 +95,7 @@ const REVIEW_RULES = Object.freeze([
|
|
|
95
95
|
regex: /\b(sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,})\b/,
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
|
+
id: "SL-SEC-005",
|
|
98
99
|
severity: "P2",
|
|
99
100
|
message: "Possible hardcoded credential literal.",
|
|
100
101
|
regex: /(api[_-]?key|secret|token)\s*[:=]\s*['\"][^'\"]{20,}['\"]/i,
|
|
@@ -336,6 +337,16 @@ function regexMatches(regex, input) {
|
|
|
336
337
|
return new RegExp(regex.source, flags).test(input);
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
function isGitHubActionsContextExpressionLiteral(line) {
|
|
341
|
+
return /(?:[:=]\s*)['"]?\s*\$\{\{\s*(?:github|secrets|vars|env|inputs|steps|needs|matrix|runner|strategy)\.[^}]+\}\}\s*['"]?/i.test(
|
|
342
|
+
String(line || "")
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function shouldSuppressRuleLine(rule, line) {
|
|
347
|
+
return rule.id === "SL-SEC-005" && isGitHubActionsContextExpressionLiteral(line);
|
|
348
|
+
}
|
|
349
|
+
|
|
339
350
|
function sanitizeLineForExcerpt(line) {
|
|
340
351
|
return String(line || "")
|
|
341
352
|
.trim()
|
|
@@ -590,6 +601,9 @@ async function scanRulesForFiles({ rootPath, filePaths, rules, maxFindings = MAX
|
|
|
590
601
|
if (!regexMatches(rule.regex, line)) {
|
|
591
602
|
continue;
|
|
592
603
|
}
|
|
604
|
+
if (shouldSuppressRuleLine(rule, line)) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
593
607
|
const pushed = tryPushFinding(
|
|
594
608
|
findings,
|
|
595
609
|
createFinding({
|
package/src/scan/generator.js
CHANGED
|
@@ -226,16 +226,22 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
226
226
|
uses: SENTINELAYER_ACTION_REF,
|
|
227
227
|
with: {
|
|
228
228
|
github_token: "${{ github.token }}",
|
|
229
|
+
openai_api_key: "${{ secrets.OPENAI_API_KEY }}",
|
|
230
|
+
google_api_key: "${{ secrets.GOOGLE_API_KEY }}",
|
|
231
|
+
llm_provider: "${{ secrets.GOOGLE_API_KEY != '' && 'google' || 'openai' }}",
|
|
229
232
|
sentinelayer_token: `\${{ secrets.${normalizedSecret} }}`,
|
|
230
|
-
sentinelayer_managed_llm:
|
|
233
|
+
sentinelayer_managed_llm: `\${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
231
234
|
scan_mode: profile.scanMode || "deep",
|
|
232
235
|
severity_gate: profile.severityGate || "P1",
|
|
233
|
-
model: "gpt-5.3-codex",
|
|
236
|
+
model: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-pro' || 'gpt-5.3-codex' }}",
|
|
234
237
|
codex_model: "gpt-5.3-codex",
|
|
235
|
-
model_fallback: "gpt-4.1-mini",
|
|
236
|
-
use_codex: "
|
|
238
|
+
model_fallback: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
|
|
239
|
+
use_codex: "${{ secrets.GOOGLE_API_KEY == '' }}",
|
|
237
240
|
codex_only: "false",
|
|
238
241
|
llm_failure_policy: "block",
|
|
242
|
+
max_daily_scans: "${{ vars.OMAR_MAX_DAILY_SCANS || '200' }}",
|
|
243
|
+
min_scan_interval_minutes: "${{ vars.OMAR_MIN_SCAN_INTERVAL_MINUTES || '0' }}",
|
|
244
|
+
rate_limit_fail_mode: "closed",
|
|
239
245
|
playwright_mode: profile.playwrightMode || "off",
|
|
240
246
|
sbom_mode: profile.sbomMode || "off",
|
|
241
247
|
wait_for_completion: "true",
|
|
@@ -288,6 +294,12 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
288
294
|
OMAR_SCAN_MODE: "${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}",
|
|
289
295
|
OMAR_SEVERITY_GATE: "${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}",
|
|
290
296
|
OMAR_P2_MAX_ALLOWED: "${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}",
|
|
297
|
+
OMAR_OPENAI_KEY_PRESENT: "${{ secrets.OPENAI_API_KEY != '' }}",
|
|
298
|
+
OMAR_GOOGLE_KEY_PRESENT: "${{ secrets.GOOGLE_API_KEY != '' }}",
|
|
299
|
+
OMAR_MANAGED_LLM: `\${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
300
|
+
OMAR_LLM_PROVIDER: "${{ secrets.GOOGLE_API_KEY != '' && 'google' || 'openai' }}",
|
|
301
|
+
OMAR_MODEL: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-pro' || 'gpt-5.3-codex' }}",
|
|
302
|
+
OMAR_MODEL_FALLBACK: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
|
|
291
303
|
},
|
|
292
304
|
run: [
|
|
293
305
|
"set -euo pipefail",
|
|
@@ -304,6 +316,9 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
304
316
|
" value = env(name, '0').strip()",
|
|
305
317
|
" return int(value) if value.isdigit() else 0",
|
|
306
318
|
"",
|
|
319
|
+
"def bool_env(name):",
|
|
320
|
+
" return env(name).strip().lower() == 'true'",
|
|
321
|
+
"",
|
|
307
322
|
"github_run_id = env('GITHUB_RUN_ID')",
|
|
308
323
|
"server_url = env('GITHUB_SERVER_URL', 'https://github.com').rstrip('/')",
|
|
309
324
|
"repository = env('GITHUB_REPOSITORY')",
|
|
@@ -326,7 +341,13 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
326
341
|
" 'scan': {",
|
|
327
342
|
" 'mode': env('OMAR_SCAN_MODE', 'deep'),",
|
|
328
343
|
` 'action_ref': '${SENTINELAYER_ACTION_REF}',`,
|
|
329
|
-
" '
|
|
344
|
+
" 'llm_provider': env('OMAR_LLM_PROVIDER', 'openai'),",
|
|
345
|
+
" 'model': env('OMAR_MODEL', 'gpt-5.3-codex'),",
|
|
346
|
+
" 'model_fallback': env('OMAR_MODEL_FALLBACK', 'gpt-4.1-mini'),",
|
|
347
|
+
" 'llm_route': 'google_api_key' if bool_env('OMAR_GOOGLE_KEY_PRESENT') else ('openai_api_key' if bool_env('OMAR_OPENAI_KEY_PRESENT') else 'sentinelayer_managed'),",
|
|
348
|
+
" 'google_key_present': bool_env('OMAR_GOOGLE_KEY_PRESENT'),",
|
|
349
|
+
" 'openai_key_present': bool_env('OMAR_OPENAI_KEY_PRESENT'),",
|
|
350
|
+
" 'managed_llm': bool_env('OMAR_MANAGED_LLM'),",
|
|
330
351
|
" 'llm_failure_policy': 'block',",
|
|
331
352
|
" },",
|
|
332
353
|
" 'github': {",
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
const SESSION_CONTROL_EVENT_TYPES = new Set([
|
|
2
|
+
"file_lock",
|
|
3
|
+
"file_unlock",
|
|
4
|
+
"file_lock_expired",
|
|
2
5
|
"listener_stop",
|
|
3
6
|
"session_coaching",
|
|
4
7
|
"session_listen_catchup",
|
|
5
8
|
"session_listen_error",
|
|
9
|
+
"session_reaction",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const QUIET_SESSION_ACTION_TYPES = new Set([
|
|
13
|
+
"ack",
|
|
14
|
+
"dislike",
|
|
15
|
+
"disregard",
|
|
16
|
+
"like",
|
|
17
|
+
"view",
|
|
6
18
|
]);
|
|
7
19
|
|
|
8
20
|
function normalizeString(value) {
|
|
@@ -28,8 +40,13 @@ export function isSessionListenerLifecycleEvent(event = {}) {
|
|
|
28
40
|
|
|
29
41
|
export function isSessionControlEvent(event = {}) {
|
|
30
42
|
const type = eventType(event);
|
|
43
|
+
const payload = event?.payload && typeof event.payload === "object" && !Array.isArray(event.payload)
|
|
44
|
+
? event.payload
|
|
45
|
+
: {};
|
|
46
|
+
const actionType = normalizeString(payload.actionType || payload.action_type || payload.reaction).toLowerCase();
|
|
31
47
|
return (
|
|
32
48
|
isSessionListenerLifecycleEvent(event) ||
|
|
49
|
+
(type === "session_action" && QUIET_SESSION_ACTION_TYPES.has(actionType)) ||
|
|
33
50
|
type.startsWith("session_listen_") ||
|
|
34
51
|
SESSION_CONTROL_EVENT_TYPES.has(type)
|
|
35
52
|
);
|
package/src/session/sync.js
CHANGED
|
@@ -253,9 +253,14 @@ function chronologicalSessionEvents(events = []) {
|
|
|
253
253
|
return (Array.isArray(events) ? events : [])
|
|
254
254
|
.map((event, index) => ({ event, index }))
|
|
255
255
|
.sort((left, right) => {
|
|
256
|
+
const leftSequence = eventSequenceNumber(left.event);
|
|
257
|
+
const rightSequence = eventSequenceNumber(right.event);
|
|
258
|
+
if (leftSequence > 0 && rightSequence > 0 && leftSequence !== rightSequence) {
|
|
259
|
+
return leftSequence - rightSequence;
|
|
260
|
+
}
|
|
256
261
|
const timeDiff = eventTimestampMs(left.event) - eventTimestampMs(right.event);
|
|
257
262
|
if (timeDiff !== 0) return timeDiff;
|
|
258
|
-
const sequenceDiff =
|
|
263
|
+
const sequenceDiff = leftSequence - rightSequence;
|
|
259
264
|
if (sequenceDiff !== 0) return sequenceDiff;
|
|
260
265
|
return left.index - right.index;
|
|
261
266
|
})
|