@devflow-tools/database 0.17.0 → 0.17.2
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/CHANGELOG.md +30 -0
- package/dist/database.d.ts +51 -0
- package/dist/database.js +429 -29
- package/dist/index.d.ts +3 -1
- package/dist/index.js +10 -1
- package/dist/node-sqlite.d.ts +1 -1
- package/dist/node-sqlite.js +8 -4
- package/dist/task-semantic-control.d.ts +79 -0
- package/dist/task-semantic-control.js +196 -0
- package/dist/work-queue.d.ts +1 -0
- package/package.json +1 -1
- package/src/database.ts +548 -35
- package/src/index.ts +19 -0
- package/src/node-sqlite.ts +8 -4
- package/src/task-semantic-control.ts +270 -0
- package/src/work-queue.ts +1 -0
package/src/database.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodeSqliteDatabase } from './node-sqlite';
|
|
2
2
|
import { join, dirname, resolve } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, realpathSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, realpathSync, statSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import type {
|
|
@@ -70,6 +70,20 @@ import {
|
|
|
70
70
|
type WorkflowWorkerRecord,
|
|
71
71
|
} from './workflow-workers';
|
|
72
72
|
import type { RetrievalLedgerEventRecord } from './retrieval-ledger';
|
|
73
|
+
import {
|
|
74
|
+
assertTaskIdentity,
|
|
75
|
+
mapChannelQueryPlanRow,
|
|
76
|
+
mapTaskIntentArtifactRow,
|
|
77
|
+
mapTerminalTransitionRow,
|
|
78
|
+
mapToolNameResolutionRow,
|
|
79
|
+
mapTranscriptCheckpointRow,
|
|
80
|
+
stableSemanticJson,
|
|
81
|
+
type ChannelQueryPlanRecord,
|
|
82
|
+
type TaskIntentArtifactRecord,
|
|
83
|
+
type TerminalTransitionRecord,
|
|
84
|
+
type ToolNameResolutionRecord,
|
|
85
|
+
type TranscriptCheckpointRecord,
|
|
86
|
+
} from './task-semantic-control';
|
|
73
87
|
|
|
74
88
|
export interface BenchmarkReportRecord {
|
|
75
89
|
runId: string;
|
|
@@ -291,35 +305,72 @@ export function getGlobalDevFlowDbPath(home = homedir()): string {
|
|
|
291
305
|
|
|
292
306
|
export function openGlobalDevFlowDatabase(
|
|
293
307
|
home = homedir(),
|
|
294
|
-
options?: { busyTimeoutMs?: number },
|
|
308
|
+
options?: { busyTimeoutMs?: number; readonly?: boolean },
|
|
295
309
|
): DevFlowDatabase {
|
|
296
310
|
return new DevFlowDatabase(home, {
|
|
297
311
|
dbPath: getGlobalDevFlowDbPath(home),
|
|
298
312
|
busyTimeoutMs: options?.busyTimeoutMs,
|
|
313
|
+
readonly: options?.readonly,
|
|
299
314
|
});
|
|
300
315
|
}
|
|
301
316
|
|
|
317
|
+
export function openGlobalDevFlowReadOnlyDatabase(
|
|
318
|
+
home = homedir(),
|
|
319
|
+
options?: { busyTimeoutMs?: number },
|
|
320
|
+
): DevFlowDatabase {
|
|
321
|
+
return openGlobalDevFlowDatabase(home, { ...options, readonly: true });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
interface InitializedDatabaseIdentity {
|
|
325
|
+
dev: number;
|
|
326
|
+
ino: number;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Schema setup is process-owned. Hook daemons open short-lived connections for
|
|
330
|
+
// bounded transactions, but replaying the full idempotent DDL on every open is
|
|
331
|
+
// still expensive and serializes concurrent host requests. An inode identity
|
|
332
|
+
// keeps the cache safe when a test, repair, or user replaces the database file.
|
|
333
|
+
const initializedDatabaseFiles = new Map<string, InitializedDatabaseIdentity>();
|
|
334
|
+
|
|
335
|
+
function getDatabaseIdentity(path: string): InitializedDatabaseIdentity | null {
|
|
336
|
+
try {
|
|
337
|
+
const stats = statSync(path);
|
|
338
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
302
344
|
export class DevFlowDatabase {
|
|
303
345
|
private db: NodeSqliteDatabase;
|
|
304
346
|
|
|
305
|
-
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number }) {
|
|
347
|
+
constructor(projectRoot: string, opts?: { dbPath?: string; busyTimeoutMs?: number; readonly?: boolean }) {
|
|
306
348
|
let dbPath: string;
|
|
307
349
|
if (opts?.dbPath) {
|
|
308
350
|
const dir = dirname(opts.dbPath);
|
|
309
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
351
|
+
if (!existsSync(dir) && !opts.readonly) mkdirSync(dir, { recursive: true });
|
|
310
352
|
dbPath = opts.dbPath;
|
|
311
353
|
} else {
|
|
312
354
|
const devflowDir = join(projectRoot, '.devflow');
|
|
313
|
-
if (!existsSync(devflowDir)) mkdirSync(devflowDir, { recursive: true });
|
|
355
|
+
if (!existsSync(devflowDir) && !opts?.readonly) mkdirSync(devflowDir, { recursive: true });
|
|
314
356
|
dbPath = join(devflowDir, 'devflow.db');
|
|
315
357
|
}
|
|
316
|
-
|
|
358
|
+
if (opts?.readonly && !existsSync(dbPath)) throw new Error(`DEVFLOW_DATABASE_NOT_FOUND:${dbPath}`);
|
|
359
|
+
this.db = new NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs, opts?.readonly === true);
|
|
317
360
|
|
|
318
|
-
this.db.exec('PRAGMA journal_mode = WAL');
|
|
319
361
|
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
|
|
362
|
+
if (opts?.readonly) return;
|
|
320
363
|
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
321
|
-
|
|
322
|
-
|
|
364
|
+
const databaseIdentity = getDatabaseIdentity(dbPath);
|
|
365
|
+
const initializedIdentity = initializedDatabaseFiles.get(dbPath);
|
|
366
|
+
const schemaInitialized = databaseIdentity !== null
|
|
367
|
+
&& initializedIdentity?.dev === databaseIdentity.dev
|
|
368
|
+
&& initializedIdentity.ino === databaseIdentity.ino;
|
|
369
|
+
if (!schemaInitialized) {
|
|
370
|
+
this.initializeSchema();
|
|
371
|
+
const currentIdentity = getDatabaseIdentity(dbPath);
|
|
372
|
+
if (currentIdentity) initializedDatabaseFiles.set(dbPath, currentIdentity);
|
|
373
|
+
}
|
|
323
374
|
}
|
|
324
375
|
|
|
325
376
|
private initializeSchema() {
|
|
@@ -567,6 +618,119 @@ export class DevFlowDatabase {
|
|
|
567
618
|
project_root, session_id, execution_id, request_id, context_receipt, source_type, source_id, created_at
|
|
568
619
|
);
|
|
569
620
|
|
|
621
|
+
CREATE TABLE IF NOT EXISTS devflow_task_intent_artifacts (
|
|
622
|
+
artifact_hash TEXT PRIMARY KEY,
|
|
623
|
+
project_root TEXT NOT NULL,
|
|
624
|
+
project_id TEXT NOT NULL,
|
|
625
|
+
host_id TEXT NOT NULL,
|
|
626
|
+
session_id TEXT NOT NULL,
|
|
627
|
+
turn_id TEXT NOT NULL,
|
|
628
|
+
request_id TEXT NOT NULL,
|
|
629
|
+
execution_id TEXT,
|
|
630
|
+
version INTEGER NOT NULL CHECK(version > 0),
|
|
631
|
+
supersedes_hash TEXT,
|
|
632
|
+
raw_prompt TEXT NOT NULL,
|
|
633
|
+
normalized_prompt TEXT NOT NULL,
|
|
634
|
+
command TEXT,
|
|
635
|
+
slash_args_json TEXT NOT NULL DEFAULT '[]',
|
|
636
|
+
active_skill TEXT,
|
|
637
|
+
intent TEXT NOT NULL,
|
|
638
|
+
action TEXT NOT NULL,
|
|
639
|
+
entities_json TEXT NOT NULL DEFAULT '[]',
|
|
640
|
+
target_anchors_json TEXT NOT NULL DEFAULT '[]',
|
|
641
|
+
policy_constraints_json TEXT NOT NULL DEFAULT '[]',
|
|
642
|
+
classification_evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
643
|
+
source_event_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
644
|
+
source_hash TEXT NOT NULL,
|
|
645
|
+
created_at INTEGER NOT NULL,
|
|
646
|
+
UNIQUE(project_root, session_id, turn_id, version),
|
|
647
|
+
UNIQUE(project_root, session_id, request_id, version)
|
|
648
|
+
);
|
|
649
|
+
CREATE INDEX IF NOT EXISTS idx_task_intent_active
|
|
650
|
+
ON devflow_task_intent_artifacts(project_root, session_id, turn_id, version DESC);
|
|
651
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_intent_source
|
|
652
|
+
ON devflow_task_intent_artifacts(project_root, session_id, source_hash, version);
|
|
653
|
+
|
|
654
|
+
CREATE TABLE IF NOT EXISTS devflow_channel_query_plans (
|
|
655
|
+
plan_hash TEXT PRIMARY KEY,
|
|
656
|
+
source_intent_hash TEXT NOT NULL,
|
|
657
|
+
project_root TEXT NOT NULL,
|
|
658
|
+
project_id TEXT NOT NULL,
|
|
659
|
+
host_id TEXT NOT NULL,
|
|
660
|
+
session_id TEXT NOT NULL,
|
|
661
|
+
turn_id TEXT NOT NULL,
|
|
662
|
+
request_id TEXT NOT NULL,
|
|
663
|
+
execution_id TEXT,
|
|
664
|
+
code_queries_json TEXT NOT NULL DEFAULT '[]',
|
|
665
|
+
memory_queries_json TEXT NOT NULL DEFAULT '[]',
|
|
666
|
+
knowledge_queries_json TEXT NOT NULL DEFAULT '[]',
|
|
667
|
+
generated_by TEXT NOT NULL
|
|
668
|
+
CHECK(generated_by IN ('deterministic', 'host_semantic', 'hybrid')),
|
|
669
|
+
degradation_json TEXT,
|
|
670
|
+
created_at INTEGER NOT NULL,
|
|
671
|
+
UNIQUE(project_root, session_id, turn_id, source_intent_hash)
|
|
672
|
+
);
|
|
673
|
+
CREATE INDEX IF NOT EXISTS idx_channel_query_plan_request
|
|
674
|
+
ON devflow_channel_query_plans(project_root, session_id, request_id, created_at DESC);
|
|
675
|
+
|
|
676
|
+
CREATE TABLE IF NOT EXISTS devflow_tool_name_resolutions (
|
|
677
|
+
id TEXT PRIMARY KEY,
|
|
678
|
+
source_hash TEXT NOT NULL UNIQUE,
|
|
679
|
+
project_root TEXT NOT NULL,
|
|
680
|
+
project_id TEXT NOT NULL,
|
|
681
|
+
host_id TEXT NOT NULL,
|
|
682
|
+
session_id TEXT NOT NULL,
|
|
683
|
+
turn_id TEXT NOT NULL,
|
|
684
|
+
request_id TEXT NOT NULL,
|
|
685
|
+
execution_id TEXT,
|
|
686
|
+
requested_name TEXT NOT NULL,
|
|
687
|
+
canonical_name TEXT,
|
|
688
|
+
projected_name TEXT,
|
|
689
|
+
status TEXT NOT NULL CHECK(status IN ('canonical', 'alias', 'unsupported')),
|
|
690
|
+
attempt INTEGER NOT NULL CHECK(attempt >= 0),
|
|
691
|
+
capability_mechanism TEXT,
|
|
692
|
+
reason TEXT,
|
|
693
|
+
created_at INTEGER NOT NULL
|
|
694
|
+
);
|
|
695
|
+
CREATE INDEX IF NOT EXISTS idx_tool_resolution_request
|
|
696
|
+
ON devflow_tool_name_resolutions(project_root, session_id, request_id, created_at);
|
|
697
|
+
|
|
698
|
+
CREATE TABLE IF NOT EXISTS devflow_terminal_transitions (
|
|
699
|
+
receipt_id TEXT PRIMARY KEY,
|
|
700
|
+
project_root TEXT NOT NULL,
|
|
701
|
+
project_id TEXT NOT NULL,
|
|
702
|
+
host_id TEXT NOT NULL,
|
|
703
|
+
session_id TEXT NOT NULL,
|
|
704
|
+
turn_id TEXT NOT NULL,
|
|
705
|
+
request_id TEXT NOT NULL,
|
|
706
|
+
execution_id TEXT,
|
|
707
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
708
|
+
from_state TEXT,
|
|
709
|
+
to_state TEXT NOT NULL,
|
|
710
|
+
source_receipt_id TEXT,
|
|
711
|
+
reason TEXT NOT NULL,
|
|
712
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
713
|
+
created_at INTEGER NOT NULL,
|
|
714
|
+
UNIQUE(project_root, session_id, turn_id, sequence)
|
|
715
|
+
);
|
|
716
|
+
CREATE INDEX IF NOT EXISTS idx_terminal_transition_turn
|
|
717
|
+
ON devflow_terminal_transitions(project_root, session_id, turn_id, sequence);
|
|
718
|
+
|
|
719
|
+
CREATE TABLE IF NOT EXISTS devflow_transcript_checkpoints (
|
|
720
|
+
id TEXT PRIMARY KEY,
|
|
721
|
+
project_root TEXT NOT NULL,
|
|
722
|
+
host_id TEXT NOT NULL,
|
|
723
|
+
session_id TEXT NOT NULL,
|
|
724
|
+
source_path TEXT NOT NULL,
|
|
725
|
+
source_hash TEXT NOT NULL,
|
|
726
|
+
byte_offset INTEGER NOT NULL CHECK(byte_offset >= 0),
|
|
727
|
+
event_count INTEGER NOT NULL CHECK(event_count >= 0),
|
|
728
|
+
created_at INTEGER NOT NULL,
|
|
729
|
+
UNIQUE(project_root, host_id, session_id, source_path, source_hash)
|
|
730
|
+
);
|
|
731
|
+
CREATE INDEX IF NOT EXISTS idx_transcript_checkpoint_latest
|
|
732
|
+
ON devflow_transcript_checkpoints(project_root, host_id, session_id, source_path, created_at DESC);
|
|
733
|
+
|
|
570
734
|
CREATE TABLE IF NOT EXISTS devflow_policy_verification_baselines (
|
|
571
735
|
project_root TEXT NOT NULL,
|
|
572
736
|
kind TEXT NOT NULL,
|
|
@@ -1423,16 +1587,30 @@ export class DevFlowDatabase {
|
|
|
1423
1587
|
}));
|
|
1424
1588
|
}
|
|
1425
1589
|
|
|
1426
|
-
getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
|
|
1590
|
+
getToolCallEventByToolUseId(sessionId: string, toolUseId: string): {
|
|
1591
|
+
eventId: string;
|
|
1592
|
+
executionId: string;
|
|
1593
|
+
timestamp: number;
|
|
1594
|
+
duration: number;
|
|
1595
|
+
input: Record<string, unknown>;
|
|
1596
|
+
} | null {
|
|
1427
1597
|
const row = this.db.prepare(`
|
|
1428
|
-
SELECT event_id, timestamp, duration
|
|
1598
|
+
SELECT event_id, execution_id, timestamp, duration, input
|
|
1429
1599
|
FROM tool_call_events
|
|
1430
1600
|
WHERE session_id = ? AND tool_use_id = ?
|
|
1431
1601
|
ORDER BY timestamp DESC
|
|
1432
1602
|
LIMIT 1
|
|
1433
|
-
`).get(sessionId, toolUseId) as {
|
|
1603
|
+
`).get(sessionId, toolUseId) as {
|
|
1604
|
+
event_id: string; execution_id: string; timestamp: number; duration: number; input: string | null;
|
|
1605
|
+
} | undefined;
|
|
1434
1606
|
return row
|
|
1435
|
-
? {
|
|
1607
|
+
? {
|
|
1608
|
+
eventId: row.event_id,
|
|
1609
|
+
executionId: row.execution_id,
|
|
1610
|
+
timestamp: row.timestamp,
|
|
1611
|
+
duration: row.duration ?? 0,
|
|
1612
|
+
input: parseJsonObject(row.input) ?? {},
|
|
1613
|
+
}
|
|
1436
1614
|
: null;
|
|
1437
1615
|
}
|
|
1438
1616
|
|
|
@@ -2933,7 +3111,8 @@ export class DevFlowDatabase {
|
|
|
2933
3111
|
SELECT COUNT(*) AS count
|
|
2934
3112
|
FROM devflow_work_items
|
|
2935
3113
|
WHERE project_root = ? AND session_id = ?
|
|
2936
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
3114
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
3115
|
+
AND attempts < max_attempts
|
|
2937
3116
|
`).get(input.projectRoot, input.sessionId) as { count?: number };
|
|
2938
3117
|
const pendingWorkCount = Number(pending?.count ?? 0);
|
|
2939
3118
|
this.db.prepare(`
|
|
@@ -3015,6 +3194,7 @@ export class DevFlowDatabase {
|
|
|
3015
3194
|
throw new Error('Work leaseMs must be a positive safe integer');
|
|
3016
3195
|
}
|
|
3017
3196
|
if (input.kinds?.length === 0) return [];
|
|
3197
|
+
if (input.workItemIds?.length === 0) return [];
|
|
3018
3198
|
|
|
3019
3199
|
const now = input.now ?? Date.now();
|
|
3020
3200
|
const leaseExpiresAt = now + input.leaseMs;
|
|
@@ -3023,9 +3203,13 @@ export class DevFlowDatabase {
|
|
|
3023
3203
|
}
|
|
3024
3204
|
const limit = Math.min(Math.floor(input.limit), 1_000);
|
|
3025
3205
|
const kinds = input.kinds ? [...new Set(input.kinds)] : undefined;
|
|
3206
|
+
const workItemIds = input.workItemIds ? [...new Set(input.workItemIds)] : undefined;
|
|
3026
3207
|
const kindClause = kinds
|
|
3027
3208
|
? `AND kind IN (${kinds.map(() => '?').join(', ')})`
|
|
3028
3209
|
: '';
|
|
3210
|
+
const workItemClause = workItemIds
|
|
3211
|
+
? `AND id IN (${workItemIds.map(() => '?').join(', ')})`
|
|
3212
|
+
: '';
|
|
3029
3213
|
|
|
3030
3214
|
this.db.exec('BEGIN IMMEDIATE');
|
|
3031
3215
|
try {
|
|
@@ -3039,9 +3223,10 @@ export class DevFlowDatabase {
|
|
|
3039
3223
|
AND next_attempt_at <= ?
|
|
3040
3224
|
AND attempts < max_attempts
|
|
3041
3225
|
${kindClause}
|
|
3226
|
+
${workItemClause}
|
|
3042
3227
|
ORDER BY next_attempt_at ASC, created_at ASC, id ASC
|
|
3043
3228
|
LIMIT ?
|
|
3044
|
-
`).all(input.projectRoot, now, ...(kinds ?? []), limit) as Array<{
|
|
3229
|
+
`).all(input.projectRoot, now, ...(kinds ?? []), ...(workItemIds ?? []), limit) as Array<{
|
|
3045
3230
|
id: string;
|
|
3046
3231
|
state: WorkState;
|
|
3047
3232
|
}>;
|
|
@@ -3251,7 +3436,8 @@ export class DevFlowDatabase {
|
|
|
3251
3436
|
SELECT COUNT(*) AS count
|
|
3252
3437
|
FROM devflow_work_items
|
|
3253
3438
|
WHERE project_root = ? AND session_id = ?
|
|
3254
|
-
AND state IN ('pending', 'leased', 'failed'
|
|
3439
|
+
AND state IN ('pending', 'leased', 'failed')
|
|
3440
|
+
AND attempts < max_attempts
|
|
3255
3441
|
AND (? IS NULL OR id <> ?)
|
|
3256
3442
|
`).get(
|
|
3257
3443
|
projectRoot,
|
|
@@ -3259,25 +3445,7 @@ export class DevFlowDatabase {
|
|
|
3259
3445
|
excludingWorkItemId ?? null,
|
|
3260
3446
|
excludingWorkItemId ?? null,
|
|
3261
3447
|
) as { count?: number };
|
|
3262
|
-
|
|
3263
|
-
SELECT COUNT(*) AS count
|
|
3264
|
-
FROM devflow_session_obligations
|
|
3265
|
-
WHERE project_root = ? AND session_id = ?
|
|
3266
|
-
AND state IN ('open', 'degraded')
|
|
3267
|
-
`).get(projectRoot, sessionId) as { count?: number };
|
|
3268
|
-
const legacyTurns = this.db.prepare(`
|
|
3269
|
-
SELECT COUNT(*) AS count
|
|
3270
|
-
FROM devflow_memory_turns t
|
|
3271
|
-
WHERE t.project_root = ? AND t.session_id = ? AND t.status = 'pending'
|
|
3272
|
-
AND NOT EXISTS (
|
|
3273
|
-
SELECT 1 FROM devflow_session_obligations o
|
|
3274
|
-
WHERE o.project_root = t.project_root AND o.session_id = t.session_id
|
|
3275
|
-
AND o.obligation_id = 'memory:' || t.turn_id
|
|
3276
|
-
)
|
|
3277
|
-
`).get(projectRoot, sessionId) as { count?: number };
|
|
3278
|
-
return Number(work?.count ?? 0)
|
|
3279
|
-
+ Number(obligations?.count ?? 0)
|
|
3280
|
-
+ Number(legacyTurns?.count ?? 0);
|
|
3448
|
+
return Number(work?.count ?? 0);
|
|
3281
3449
|
}
|
|
3282
3450
|
|
|
3283
3451
|
// ---- Hook Lifecycle ----
|
|
@@ -3685,6 +3853,293 @@ export class DevFlowDatabase {
|
|
|
3685
3853
|
).changes > 0;
|
|
3686
3854
|
}
|
|
3687
3855
|
|
|
3856
|
+
appendTaskIntentArtifact(record: TaskIntentArtifactRecord): TaskIntentArtifactRecord {
|
|
3857
|
+
assertTaskIdentity(record);
|
|
3858
|
+
if (!Number.isSafeInteger(record.version) || record.version <= 0) {
|
|
3859
|
+
throw new Error('TASK_INTENT_INVALID_VERSION');
|
|
3860
|
+
}
|
|
3861
|
+
const existingByHash = this.getTaskIntentArtifact(record.artifactHash);
|
|
3862
|
+
if (existingByHash) {
|
|
3863
|
+
if (stableSemanticJson(existingByHash) !== stableSemanticJson(record)) {
|
|
3864
|
+
throw new Error(`TASK_INTENT_HASH_CONFLICT:${record.artifactHash}`);
|
|
3865
|
+
}
|
|
3866
|
+
return existingByHash;
|
|
3867
|
+
}
|
|
3868
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
3869
|
+
try {
|
|
3870
|
+
const conflicting = this.db.prepare(`
|
|
3871
|
+
SELECT * FROM devflow_task_intent_artifacts
|
|
3872
|
+
WHERE project_root = ? AND session_id = ? AND turn_id = ? AND version = ?
|
|
3873
|
+
`).get(record.projectRoot, record.sessionId, record.turnId, record.version) as Record<string, unknown> | undefined;
|
|
3874
|
+
if (conflicting) throw new Error(`TASK_INTENT_VERSION_CONFLICT:${record.turnId}:${record.version}`);
|
|
3875
|
+
if (record.supersedesHash && !this.getTaskIntentArtifact(record.supersedesHash)) {
|
|
3876
|
+
throw new Error(`TASK_INTENT_SUPERSEDED_NOT_FOUND:${record.supersedesHash}`);
|
|
3877
|
+
}
|
|
3878
|
+
this.db.prepare(`
|
|
3879
|
+
INSERT INTO devflow_task_intent_artifacts (
|
|
3880
|
+
artifact_hash, project_root, project_id, host_id, session_id, turn_id, request_id,
|
|
3881
|
+
execution_id, version, supersedes_hash, raw_prompt, normalized_prompt, command,
|
|
3882
|
+
slash_args_json, active_skill, intent, action, entities_json, target_anchors_json,
|
|
3883
|
+
policy_constraints_json, classification_evidence_json, source_event_ids_json,
|
|
3884
|
+
source_hash, created_at
|
|
3885
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3886
|
+
`).run(
|
|
3887
|
+
record.artifactHash, record.projectRoot, record.projectId, record.hostId,
|
|
3888
|
+
record.sessionId, record.turnId, record.requestId, record.executionId ?? null,
|
|
3889
|
+
record.version, record.supersedesHash ?? null, record.rawPrompt, record.normalizedPrompt,
|
|
3890
|
+
record.command ?? null, stableSemanticJson(record.slashArgs), record.activeSkill ?? null,
|
|
3891
|
+
record.intent, record.action, stableSemanticJson(record.entities),
|
|
3892
|
+
stableSemanticJson(record.targetAnchors), stableSemanticJson(record.policyConstraints),
|
|
3893
|
+
stableSemanticJson(record.classificationEvidence), stableSemanticJson(record.sourceEventIds),
|
|
3894
|
+
record.sourceHash, record.createdAt,
|
|
3895
|
+
);
|
|
3896
|
+
const created = this.getTaskIntentArtifact(record.artifactHash)!;
|
|
3897
|
+
this.db.exec('COMMIT');
|
|
3898
|
+
return created;
|
|
3899
|
+
} catch (error) {
|
|
3900
|
+
try { this.db.exec('ROLLBACK'); } catch {}
|
|
3901
|
+
throw error;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
getTaskIntentArtifact(artifactHash: string): TaskIntentArtifactRecord | null {
|
|
3906
|
+
const row = this.db.prepare(`
|
|
3907
|
+
SELECT * FROM devflow_task_intent_artifacts WHERE artifact_hash = ?
|
|
3908
|
+
`).get(artifactHash) as Record<string, unknown> | undefined;
|
|
3909
|
+
return row ? mapTaskIntentArtifactRow(row) : null;
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
getLatestTaskIntentArtifact(input: {
|
|
3913
|
+
projectRoot: string;
|
|
3914
|
+
sessionId: string;
|
|
3915
|
+
turnId?: string;
|
|
3916
|
+
requestId?: string;
|
|
3917
|
+
}): TaskIntentArtifactRecord | null {
|
|
3918
|
+
if (!input.turnId && !input.requestId) throw new Error('TASK_INTENT_LOOKUP_IDENTITY_REQUIRED');
|
|
3919
|
+
const predicates = ['project_root = ?', 'session_id = ?'];
|
|
3920
|
+
const params: unknown[] = [input.projectRoot, input.sessionId];
|
|
3921
|
+
if (input.turnId) { predicates.push('turn_id = ?'); params.push(input.turnId); }
|
|
3922
|
+
if (input.requestId) { predicates.push('request_id = ?'); params.push(input.requestId); }
|
|
3923
|
+
const row = this.db.prepare(`
|
|
3924
|
+
SELECT * FROM devflow_task_intent_artifacts
|
|
3925
|
+
WHERE ${predicates.join(' AND ')} ORDER BY version DESC, created_at DESC LIMIT 1
|
|
3926
|
+
`).get(...params) as Record<string, unknown> | undefined;
|
|
3927
|
+
return row ? mapTaskIntentArtifactRow(row) : null;
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
getTaskIntentArtifactBySource(input: {
|
|
3931
|
+
projectRoot: string;
|
|
3932
|
+
sessionId: string;
|
|
3933
|
+
sourceHash: string;
|
|
3934
|
+
}): TaskIntentArtifactRecord | null {
|
|
3935
|
+
const row = this.db.prepare(`
|
|
3936
|
+
SELECT * FROM devflow_task_intent_artifacts
|
|
3937
|
+
WHERE project_root = ? AND session_id = ? AND source_hash = ?
|
|
3938
|
+
ORDER BY version DESC, created_at DESC LIMIT 1
|
|
3939
|
+
`).get(input.projectRoot, input.sessionId, input.sourceHash) as Record<string, unknown> | undefined;
|
|
3940
|
+
return row ? mapTaskIntentArtifactRow(row) : null;
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
listLatestTaskIntentArtifacts(projectRoot: string, sessionId: string): TaskIntentArtifactRecord[] {
|
|
3944
|
+
return (this.db.prepare(`
|
|
3945
|
+
SELECT artifact.* FROM devflow_task_intent_artifacts artifact
|
|
3946
|
+
JOIN (
|
|
3947
|
+
SELECT turn_id, MAX(version) AS version
|
|
3948
|
+
FROM devflow_task_intent_artifacts
|
|
3949
|
+
WHERE project_root = ? AND session_id = ? GROUP BY turn_id
|
|
3950
|
+
) latest ON latest.turn_id = artifact.turn_id AND latest.version = artifact.version
|
|
3951
|
+
WHERE artifact.project_root = ? AND artifact.session_id = ?
|
|
3952
|
+
ORDER BY artifact.created_at ASC, artifact.turn_id ASC
|
|
3953
|
+
`).all(projectRoot, sessionId, projectRoot, sessionId) as Array<Record<string, unknown>>)
|
|
3954
|
+
.map(mapTaskIntentArtifactRow);
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
appendChannelQueryPlan(record: ChannelQueryPlanRecord): ChannelQueryPlanRecord {
|
|
3958
|
+
assertTaskIdentity(record);
|
|
3959
|
+
const source = this.getTaskIntentArtifact(record.sourceIntentHash);
|
|
3960
|
+
if (!source) throw new Error(`QUERY_PLAN_INTENT_NOT_FOUND:${record.sourceIntentHash}`);
|
|
3961
|
+
if (!sameTaskIdentity(source, record)) throw new Error('QUERY_PLAN_IDENTITY_MISMATCH');
|
|
3962
|
+
const existing = this.getChannelQueryPlan(record.planHash);
|
|
3963
|
+
if (existing) {
|
|
3964
|
+
if (stableSemanticJson(existing) !== stableSemanticJson(record)) {
|
|
3965
|
+
throw new Error(`QUERY_PLAN_HASH_CONFLICT:${record.planHash}`);
|
|
3966
|
+
}
|
|
3967
|
+
return existing;
|
|
3968
|
+
}
|
|
3969
|
+
try {
|
|
3970
|
+
this.db.prepare(`
|
|
3971
|
+
INSERT INTO devflow_channel_query_plans (
|
|
3972
|
+
plan_hash, source_intent_hash, project_root, project_id, host_id, session_id,
|
|
3973
|
+
turn_id, request_id, execution_id, code_queries_json, memory_queries_json,
|
|
3974
|
+
knowledge_queries_json, generated_by, degradation_json, created_at
|
|
3975
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3976
|
+
`).run(
|
|
3977
|
+
record.planHash, record.sourceIntentHash, record.projectRoot, record.projectId,
|
|
3978
|
+
record.hostId, record.sessionId, record.turnId, record.requestId,
|
|
3979
|
+
record.executionId ?? null, stableSemanticJson(record.code),
|
|
3980
|
+
stableSemanticJson(record.memory), stableSemanticJson(record.knowledge),
|
|
3981
|
+
record.generatedBy, record.degradation ? stableSemanticJson(record.degradation) : null,
|
|
3982
|
+
record.createdAt,
|
|
3983
|
+
);
|
|
3984
|
+
} catch (error) {
|
|
3985
|
+
if (String(error).includes('UNIQUE constraint failed')) {
|
|
3986
|
+
throw new Error(`QUERY_PLAN_SOURCE_CONFLICT:${record.sourceIntentHash}`);
|
|
3987
|
+
}
|
|
3988
|
+
throw error;
|
|
3989
|
+
}
|
|
3990
|
+
return this.getChannelQueryPlan(record.planHash)!;
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
getChannelQueryPlan(planHash: string): ChannelQueryPlanRecord | null {
|
|
3994
|
+
const row = this.db.prepare(`
|
|
3995
|
+
SELECT * FROM devflow_channel_query_plans WHERE plan_hash = ?
|
|
3996
|
+
`).get(planHash) as Record<string, unknown> | undefined;
|
|
3997
|
+
return row ? mapChannelQueryPlanRow(row) : null;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
getChannelQueryPlanForIntent(input: {
|
|
4001
|
+
projectRoot: string;
|
|
4002
|
+
sessionId: string;
|
|
4003
|
+
turnId: string;
|
|
4004
|
+
sourceIntentHash: string;
|
|
4005
|
+
}): ChannelQueryPlanRecord | null {
|
|
4006
|
+
const row = this.db.prepare(`
|
|
4007
|
+
SELECT * FROM devflow_channel_query_plans
|
|
4008
|
+
WHERE project_root = ? AND session_id = ? AND turn_id = ? AND source_intent_hash = ?
|
|
4009
|
+
ORDER BY created_at DESC LIMIT 1
|
|
4010
|
+
`).get(
|
|
4011
|
+
input.projectRoot,
|
|
4012
|
+
input.sessionId,
|
|
4013
|
+
input.turnId,
|
|
4014
|
+
input.sourceIntentHash,
|
|
4015
|
+
) as Record<string, unknown> | undefined;
|
|
4016
|
+
return row ? mapChannelQueryPlanRow(row) : null;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
appendToolNameResolution(record: ToolNameResolutionRecord): boolean {
|
|
4020
|
+
assertTaskIdentity(record);
|
|
4021
|
+
if (!Number.isSafeInteger(record.attempt) || record.attempt < 0) {
|
|
4022
|
+
throw new Error('TOOL_RESOLUTION_INVALID_ATTEMPT');
|
|
4023
|
+
}
|
|
4024
|
+
return this.db.prepare(`
|
|
4025
|
+
INSERT OR IGNORE INTO devflow_tool_name_resolutions (
|
|
4026
|
+
id, source_hash, project_root, project_id, host_id, session_id, turn_id,
|
|
4027
|
+
request_id, execution_id, requested_name, canonical_name, projected_name,
|
|
4028
|
+
status, attempt, capability_mechanism, reason, created_at
|
|
4029
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4030
|
+
`).run(
|
|
4031
|
+
record.id, record.sourceHash, record.projectRoot, record.projectId, record.hostId,
|
|
4032
|
+
record.sessionId, record.turnId, record.requestId, record.executionId ?? null,
|
|
4033
|
+
record.requestedName, record.canonicalName ?? null, record.projectedName ?? null,
|
|
4034
|
+
record.status, record.attempt, record.capabilityMechanism ?? null,
|
|
4035
|
+
record.reason ?? null, record.createdAt,
|
|
4036
|
+
).changes > 0;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
listToolNameResolutions(input: {
|
|
4040
|
+
projectRoot: string;
|
|
4041
|
+
sessionId: string;
|
|
4042
|
+
requestId?: string;
|
|
4043
|
+
}): ToolNameResolutionRecord[] {
|
|
4044
|
+
const predicates = ['project_root = ?', 'session_id = ?'];
|
|
4045
|
+
const params: unknown[] = [input.projectRoot, input.sessionId];
|
|
4046
|
+
if (input.requestId) { predicates.push('request_id = ?'); params.push(input.requestId); }
|
|
4047
|
+
return (this.db.prepare(`
|
|
4048
|
+
SELECT * FROM devflow_tool_name_resolutions WHERE ${predicates.join(' AND ')}
|
|
4049
|
+
ORDER BY created_at ASC, attempt ASC, id ASC
|
|
4050
|
+
`).all(...params) as Array<Record<string, unknown>>).map(mapToolNameResolutionRow);
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
appendTerminalTransition(record: TerminalTransitionRecord): TerminalTransitionRecord {
|
|
4054
|
+
assertTaskIdentity(record);
|
|
4055
|
+
if (!Number.isSafeInteger(record.sequence) || record.sequence <= 0) {
|
|
4056
|
+
throw new Error('TERMINAL_TRANSITION_INVALID_SEQUENCE');
|
|
4057
|
+
}
|
|
4058
|
+
const existing = this.getTerminalTransition(record.receiptId);
|
|
4059
|
+
if (existing) {
|
|
4060
|
+
if (stableSemanticJson(existing) !== stableSemanticJson(record)) {
|
|
4061
|
+
throw new Error(`TERMINAL_RECEIPT_CONFLICT:${record.receiptId}`);
|
|
4062
|
+
}
|
|
4063
|
+
return existing;
|
|
4064
|
+
}
|
|
4065
|
+
const latest = this.getLatestTerminalTransition(record.projectRoot, record.sessionId, record.turnId);
|
|
4066
|
+
if (record.sequence !== (latest?.sequence ?? 0) + 1) {
|
|
4067
|
+
throw new Error(`TERMINAL_TRANSITION_SEQUENCE:${latest?.sequence ?? 0}->${record.sequence}`);
|
|
4068
|
+
}
|
|
4069
|
+
if ((latest?.toState ?? undefined) !== record.fromState) {
|
|
4070
|
+
throw new Error(`TERMINAL_TRANSITION_FROM_MISMATCH:${record.fromState ?? 'none'}`);
|
|
4071
|
+
}
|
|
4072
|
+
this.db.prepare(`
|
|
4073
|
+
INSERT INTO devflow_terminal_transitions (
|
|
4074
|
+
receipt_id, project_root, project_id, host_id, session_id, turn_id, request_id,
|
|
4075
|
+
execution_id, sequence, from_state, to_state, source_receipt_id, reason,
|
|
4076
|
+
payload_json, created_at
|
|
4077
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4078
|
+
`).run(
|
|
4079
|
+
record.receiptId, record.projectRoot, record.projectId, record.hostId,
|
|
4080
|
+
record.sessionId, record.turnId, record.requestId, record.executionId ?? null,
|
|
4081
|
+
record.sequence, record.fromState ?? null, record.toState,
|
|
4082
|
+
record.sourceReceiptId ?? null, record.reason, stableSemanticJson(record.payload),
|
|
4083
|
+
record.createdAt,
|
|
4084
|
+
);
|
|
4085
|
+
return this.getTerminalTransition(record.receiptId)!;
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
getTerminalTransition(receiptId: string): TerminalTransitionRecord | null {
|
|
4089
|
+
const row = this.db.prepare(`
|
|
4090
|
+
SELECT * FROM devflow_terminal_transitions WHERE receipt_id = ?
|
|
4091
|
+
`).get(receiptId) as Record<string, unknown> | undefined;
|
|
4092
|
+
return row ? mapTerminalTransitionRow(row) : null;
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
getLatestTerminalTransition(
|
|
4096
|
+
projectRoot: string,
|
|
4097
|
+
sessionId: string,
|
|
4098
|
+
turnId: string,
|
|
4099
|
+
): TerminalTransitionRecord | null {
|
|
4100
|
+
const row = this.db.prepare(`
|
|
4101
|
+
SELECT * FROM devflow_terminal_transitions
|
|
4102
|
+
WHERE project_root = ? AND session_id = ? AND turn_id = ?
|
|
4103
|
+
ORDER BY sequence DESC LIMIT 1
|
|
4104
|
+
`).get(projectRoot, sessionId, turnId) as Record<string, unknown> | undefined;
|
|
4105
|
+
return row ? mapTerminalTransitionRow(row) : null;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
appendTranscriptCheckpoint(record: TranscriptCheckpointRecord): boolean {
|
|
4109
|
+
if (!record.id || !record.projectRoot || !record.hostId || !record.sessionId
|
|
4110
|
+
|| !record.sourcePath || !record.sourceHash) {
|
|
4111
|
+
throw new Error('TRANSCRIPT_CHECKPOINT_INVALID_IDENTITY');
|
|
4112
|
+
}
|
|
4113
|
+
if (!Number.isSafeInteger(record.byteOffset) || record.byteOffset < 0
|
|
4114
|
+
|| !Number.isSafeInteger(record.eventCount) || record.eventCount < 0) {
|
|
4115
|
+
throw new Error('TRANSCRIPT_CHECKPOINT_INVALID_POSITION');
|
|
4116
|
+
}
|
|
4117
|
+
return this.db.prepare(`
|
|
4118
|
+
INSERT OR IGNORE INTO devflow_transcript_checkpoints (
|
|
4119
|
+
id, project_root, host_id, session_id, source_path, source_hash,
|
|
4120
|
+
byte_offset, event_count, created_at
|
|
4121
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4122
|
+
`).run(
|
|
4123
|
+
record.id, record.projectRoot, record.hostId, record.sessionId,
|
|
4124
|
+
record.sourcePath, record.sourceHash, record.byteOffset, record.eventCount,
|
|
4125
|
+
record.createdAt,
|
|
4126
|
+
).changes > 0;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
getLatestTranscriptCheckpoint(input: {
|
|
4130
|
+
projectRoot: string;
|
|
4131
|
+
hostId: string;
|
|
4132
|
+
sessionId: string;
|
|
4133
|
+
sourcePath: string;
|
|
4134
|
+
}): TranscriptCheckpointRecord | null {
|
|
4135
|
+
const row = this.db.prepare(`
|
|
4136
|
+
SELECT * FROM devflow_transcript_checkpoints
|
|
4137
|
+
WHERE project_root = ? AND host_id = ? AND session_id = ? AND source_path = ?
|
|
4138
|
+
ORDER BY created_at DESC, byte_offset DESC LIMIT 1
|
|
4139
|
+
`).get(input.projectRoot, input.hostId, input.sessionId, input.sourcePath) as Record<string, unknown> | undefined;
|
|
4140
|
+
return row ? mapTranscriptCheckpointRow(row) : null;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
3688
4143
|
listRetrievalLedgerEvents(options: {
|
|
3689
4144
|
projectRoot: string; sessionId?: string; executionId?: string; requestId?: string;
|
|
3690
4145
|
}): RetrievalLedgerEventRecord[] {
|
|
@@ -3916,6 +4371,23 @@ export class DevFlowDatabase {
|
|
|
3916
4371
|
);
|
|
3917
4372
|
if (!existing) throw new Error(`Session obligation ${input.obligationId} does not exist`);
|
|
3918
4373
|
if (existing.state !== 'open') {
|
|
4374
|
+
if (existing.state === 'degraded' && input.state === 'satisfied' && input.receiptId) {
|
|
4375
|
+
const resolvedAt = input.resolvedAt ?? Date.now();
|
|
4376
|
+
this.db.prepare(`
|
|
4377
|
+
UPDATE devflow_session_obligations
|
|
4378
|
+
SET state = 'satisfied', receipt_id = ?, reason = ?, resolved_at = ?, updated_at = ?
|
|
4379
|
+
WHERE project_root = ? AND session_id = ? AND obligation_id = ? AND state = 'degraded'
|
|
4380
|
+
`).run(
|
|
4381
|
+
input.receiptId,
|
|
4382
|
+
input.reason ?? 'superseded_by_corrected_evidence',
|
|
4383
|
+
resolvedAt,
|
|
4384
|
+
resolvedAt,
|
|
4385
|
+
input.projectRoot,
|
|
4386
|
+
input.sessionId,
|
|
4387
|
+
input.obligationId,
|
|
4388
|
+
);
|
|
4389
|
+
return this.getSessionObligation(input.projectRoot, input.sessionId, input.obligationId)!;
|
|
4390
|
+
}
|
|
3919
4391
|
const sameResolution = existing.state === input.state
|
|
3920
4392
|
&& (input.receiptId === undefined || existing.receiptId === input.receiptId);
|
|
3921
4393
|
if (sameResolution) return existing;
|
|
@@ -4003,6 +4475,28 @@ export class DevFlowDatabase {
|
|
|
4003
4475
|
return turn;
|
|
4004
4476
|
}
|
|
4005
4477
|
|
|
4478
|
+
updateCommittedMemoryTurnProjection(input: {
|
|
4479
|
+
turnId: string;
|
|
4480
|
+
memoryIds: string[];
|
|
4481
|
+
reason?: string;
|
|
4482
|
+
}): MemoryTurnRecord {
|
|
4483
|
+
const turnId = normalizeTurnId(input.turnId);
|
|
4484
|
+
this.db.prepare(`
|
|
4485
|
+
UPDATE devflow_memory_turns
|
|
4486
|
+
SET memory_ids = ?, reason = COALESCE(?, reason)
|
|
4487
|
+
WHERE turn_id = ? AND status = 'committed' AND source = 'explicit_intent'
|
|
4488
|
+
`).run(
|
|
4489
|
+
JSON.stringify([...new Set(input.memoryIds)]),
|
|
4490
|
+
input.reason ?? null,
|
|
4491
|
+
turnId,
|
|
4492
|
+
);
|
|
4493
|
+
const turn = this.getMemoryTurn(turnId);
|
|
4494
|
+
if (!turn || turn.status !== 'committed') {
|
|
4495
|
+
throw new Error(`Committed explicit memory turn ${turnId} does not exist`);
|
|
4496
|
+
}
|
|
4497
|
+
return turn;
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4006
4500
|
skipMemoryTurn(input: {
|
|
4007
4501
|
turnId: string;
|
|
4008
4502
|
receiptId: string;
|
|
@@ -4924,3 +5418,22 @@ function canonicalWorkflowProjectRoot(projectRoot: string): string {
|
|
|
4924
5418
|
const resolved = resolve(projectRoot);
|
|
4925
5419
|
try { return realpathSync(resolved); } catch { return resolved; }
|
|
4926
5420
|
}
|
|
5421
|
+
|
|
5422
|
+
function sameTaskIdentity(
|
|
5423
|
+
left: {
|
|
5424
|
+
projectRoot: string; projectId: string; hostId: string; sessionId: string;
|
|
5425
|
+
turnId: string; requestId: string; executionId?: string;
|
|
5426
|
+
},
|
|
5427
|
+
right: {
|
|
5428
|
+
projectRoot: string; projectId: string; hostId: string; sessionId: string;
|
|
5429
|
+
turnId: string; requestId: string; executionId?: string;
|
|
5430
|
+
},
|
|
5431
|
+
): boolean {
|
|
5432
|
+
return left.projectRoot === right.projectRoot
|
|
5433
|
+
&& left.projectId === right.projectId
|
|
5434
|
+
&& left.hostId === right.hostId
|
|
5435
|
+
&& left.sessionId === right.sessionId
|
|
5436
|
+
&& left.turnId === right.turnId
|
|
5437
|
+
&& left.requestId === right.requestId
|
|
5438
|
+
&& left.executionId === right.executionId;
|
|
5439
|
+
}
|