@axiom-lattice/local-stores 2.0.10 → 3.0.1
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +60 -0
- package/dist/index.d.mts +33 -4
- package/dist/index.d.ts +33 -4
- package/dist/index.js +515 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +515 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/LocalA2AApiKeyStore.test.ts +119 -0
- package/src/__tests__/LocalChannelInstallationStore.test.ts +38 -0
- package/src/__tests__/LocalProjectStore.test.ts +127 -0
- package/src/__tests__/LocalTaskStore.test.ts +284 -2
- package/src/__tests__/LocalTaskWorkItemStore.test.ts +215 -0
- package/src/database.ts +8 -6
- package/src/stores/LocalA2AApiKeyStore.ts +57 -14
- package/src/stores/LocalChannelInstallationStore.ts +1 -9
- package/src/stores/LocalProjectStore.ts +30 -4
- package/src/stores/LocalTaskStore.ts +208 -7
- package/src/stores/LocalTaskWorkItemStore.ts +47 -3
package/dist/index.mjs
CHANGED
|
@@ -28,8 +28,9 @@ var DatabaseWrapper = class {
|
|
|
28
28
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
29
|
run(sql, ...params) {
|
|
30
30
|
this.db.run(sql, params);
|
|
31
|
+
const changes = this.db.getRowsModified();
|
|
31
32
|
this.save();
|
|
32
|
-
return new RunResult(
|
|
33
|
+
return new RunResult(changes);
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* Prepare and execute a query returning all matching rows as objects.
|
|
@@ -114,19 +115,20 @@ var StatementWrapper = class {
|
|
|
114
115
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
116
|
run(...params) {
|
|
116
117
|
this.db.run(this.sql, params);
|
|
118
|
+
const changes = this.db.getRowsModified();
|
|
117
119
|
if (this.parent) {
|
|
118
120
|
this.parent.save();
|
|
119
121
|
}
|
|
120
|
-
return new RunResult(
|
|
122
|
+
return new RunResult(changes);
|
|
121
123
|
}
|
|
122
124
|
};
|
|
123
125
|
var RunResult = class {
|
|
124
|
-
constructor(
|
|
125
|
-
this.
|
|
126
|
+
constructor(modifiedRows) {
|
|
127
|
+
this.modifiedRows = modifiedRows;
|
|
126
128
|
}
|
|
127
129
|
/** Number of rows modified by the last INSERT/UPDATE/DELETE. */
|
|
128
130
|
get changes() {
|
|
129
|
-
return this.
|
|
131
|
+
return this.modifiedRows;
|
|
130
132
|
}
|
|
131
133
|
};
|
|
132
134
|
async function initDatabase(options = {}) {
|
|
@@ -594,6 +596,7 @@ CREATE TABLE IF NOT EXISTS lt_projects (
|
|
|
594
596
|
name TEXT NOT NULL,
|
|
595
597
|
description TEXT,
|
|
596
598
|
config TEXT,
|
|
599
|
+
kind TEXT DEFAULT 'business',
|
|
597
600
|
created_at TEXT NOT NULL,
|
|
598
601
|
updated_at TEXT NOT NULL,
|
|
599
602
|
PRIMARY KEY (tenant_id, id)
|
|
@@ -604,8 +607,15 @@ var LocalProjectStore = class {
|
|
|
604
607
|
constructor(db) {
|
|
605
608
|
this.db = db;
|
|
606
609
|
ensureTable(db, DDL4);
|
|
610
|
+
this.ensureColumn("lt_projects", "kind", "kind TEXT DEFAULT 'business'");
|
|
607
611
|
}
|
|
608
|
-
async getProjectsByWorkspace(tenantId, workspaceId) {
|
|
612
|
+
async getProjectsByWorkspace(tenantId, workspaceId, filter) {
|
|
613
|
+
if (filter?.kind !== void 0) {
|
|
614
|
+
const rows2 = this.db.prepare(
|
|
615
|
+
`SELECT * FROM lt_projects WHERE tenant_id = ? AND workspace_id = ? AND kind = ? ORDER BY created_at DESC`
|
|
616
|
+
).all(tenantId, workspaceId, filter.kind);
|
|
617
|
+
return rows2.map(mapRowToProject);
|
|
618
|
+
}
|
|
609
619
|
const rows = this.db.prepare(
|
|
610
620
|
`SELECT * FROM lt_projects WHERE tenant_id = ? AND workspace_id = ? ORDER BY created_at DESC`
|
|
611
621
|
).all(tenantId, workspaceId);
|
|
@@ -619,16 +629,18 @@ var LocalProjectStore = class {
|
|
|
619
629
|
}
|
|
620
630
|
async createProject(tenantId, workspaceId, id, data) {
|
|
621
631
|
const now = nowISO();
|
|
632
|
+
const kind = data.kind || "business";
|
|
622
633
|
this.db.prepare(
|
|
623
|
-
`INSERT INTO lt_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
|
|
624
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
634
|
+
`INSERT INTO lt_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
|
|
635
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
625
636
|
ON CONFLICT(tenant_id, id) DO UPDATE SET
|
|
626
637
|
workspace_id = excluded.workspace_id,
|
|
627
638
|
name = excluded.name,
|
|
628
639
|
description = excluded.description,
|
|
629
640
|
config = excluded.config,
|
|
641
|
+
kind = excluded.kind,
|
|
630
642
|
updated_at = excluded.updated_at`
|
|
631
|
-
).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, now, now);
|
|
643
|
+
).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, kind, now, now);
|
|
632
644
|
return {
|
|
633
645
|
id,
|
|
634
646
|
tenantId,
|
|
@@ -636,6 +648,7 @@ var LocalProjectStore = class {
|
|
|
636
648
|
name: data.name,
|
|
637
649
|
description: data.description,
|
|
638
650
|
config: data.config,
|
|
651
|
+
kind,
|
|
639
652
|
createdAt: parseISO(now),
|
|
640
653
|
updatedAt: parseISO(now)
|
|
641
654
|
};
|
|
@@ -657,6 +670,10 @@ var LocalProjectStore = class {
|
|
|
657
670
|
setClauses.push("config = ?");
|
|
658
671
|
values.push(updates.config ? JSON.stringify(updates.config) : null);
|
|
659
672
|
}
|
|
673
|
+
if (updates.kind !== void 0) {
|
|
674
|
+
setClauses.push("kind = ?");
|
|
675
|
+
values.push(updates.kind);
|
|
676
|
+
}
|
|
660
677
|
if (setClauses.length === 0) return existing;
|
|
661
678
|
const now = nowISO();
|
|
662
679
|
setClauses.push("updated_at = ?");
|
|
@@ -673,6 +690,13 @@ var LocalProjectStore = class {
|
|
|
673
690
|
).run(tenantId, id);
|
|
674
691
|
return result.changes > 0;
|
|
675
692
|
}
|
|
693
|
+
/** Add a column if it does not exist (SQLite version compatible). */
|
|
694
|
+
ensureColumn(table, column, ddl) {
|
|
695
|
+
const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
696
|
+
if (!cols.some((c) => c.name === column)) {
|
|
697
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
676
700
|
};
|
|
677
701
|
function mapRowToProject(row) {
|
|
678
702
|
return {
|
|
@@ -682,6 +706,7 @@ function mapRowToProject(row) {
|
|
|
682
706
|
name: row.name,
|
|
683
707
|
description: row.description || void 0,
|
|
684
708
|
config: row.config ? JSON.parse(row.config) : void 0,
|
|
709
|
+
kind: row.kind || "business",
|
|
685
710
|
createdAt: parseISO(row.created_at),
|
|
686
711
|
updatedAt: parseISO(row.updated_at)
|
|
687
712
|
};
|
|
@@ -2775,15 +2800,7 @@ var LocalChannelInstallationStore = class {
|
|
|
2775
2800
|
this.db.prepare(
|
|
2776
2801
|
`INSERT INTO lt_channel_installations
|
|
2777
2802
|
(id, tenant_id, channel, name, config, enabled, fallback_agent_id, reject_when_no_binding, created_at, updated_at)
|
|
2778
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2779
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
2780
|
-
channel = excluded.channel,
|
|
2781
|
-
name = excluded.name,
|
|
2782
|
-
config = excluded.config,
|
|
2783
|
-
enabled = excluded.enabled,
|
|
2784
|
-
fallback_agent_id = excluded.fallback_agent_id,
|
|
2785
|
-
reject_when_no_binding = excluded.reject_when_no_binding,
|
|
2786
|
-
updated_at = excluded.updated_at`
|
|
2803
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2787
2804
|
).run(
|
|
2788
2805
|
installationId,
|
|
2789
2806
|
tenantId,
|
|
@@ -2875,7 +2892,7 @@ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
|
|
|
2875
2892
|
key_value TEXT NOT NULL,
|
|
2876
2893
|
tenant_id TEXT NOT NULL,
|
|
2877
2894
|
project_id TEXT,
|
|
2878
|
-
|
|
2895
|
+
assistant_ids TEXT,
|
|
2879
2896
|
label TEXT,
|
|
2880
2897
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2881
2898
|
created_at TEXT NOT NULL,
|
|
@@ -2885,10 +2902,41 @@ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
|
|
|
2885
2902
|
function generateApiKey() {
|
|
2886
2903
|
return `a2a_${randomUUID3().replace(/-/g, "")}`;
|
|
2887
2904
|
}
|
|
2905
|
+
function parseAssistantIds(raw) {
|
|
2906
|
+
if (!raw) return void 0;
|
|
2907
|
+
try {
|
|
2908
|
+
const parsed = JSON.parse(raw);
|
|
2909
|
+
return Array.isArray(parsed) ? parsed : void 0;
|
|
2910
|
+
} catch {
|
|
2911
|
+
return void 0;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2888
2914
|
var LocalA2AApiKeyStore = class {
|
|
2889
2915
|
constructor(db) {
|
|
2890
2916
|
this.db = db;
|
|
2891
2917
|
ensureTable(db, DDL16);
|
|
2918
|
+
this.ensureColumn("lt_a2a_api_keys", "assistant_ids", "assistant_ids TEXT");
|
|
2919
|
+
this.repairManagementIds();
|
|
2920
|
+
}
|
|
2921
|
+
/** Add a column if it does not exist (SQLite version compatible). */
|
|
2922
|
+
ensureColumn(table, column, ddl) {
|
|
2923
|
+
const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
2924
|
+
if (!cols.some((c) => c.name === column)) {
|
|
2925
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
repairManagementIds() {
|
|
2929
|
+
const rows = this.db.prepare(
|
|
2930
|
+
`SELECT rowid, id FROM lt_a2a_api_keys`
|
|
2931
|
+
).all();
|
|
2932
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2933
|
+
for (const row of rows) {
|
|
2934
|
+
if (!row.id || !uuidPattern.test(row.id)) {
|
|
2935
|
+
this.db.prepare(
|
|
2936
|
+
`UPDATE lt_a2a_api_keys SET id = ? WHERE rowid = ?`
|
|
2937
|
+
).run(randomUUID3(), row.rowid);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2892
2940
|
}
|
|
2893
2941
|
async findByKey(key) {
|
|
2894
2942
|
const rows = this.db.prepare(
|
|
@@ -2902,6 +2950,12 @@ var LocalA2AApiKeyStore = class {
|
|
|
2902
2950
|
}
|
|
2903
2951
|
return null;
|
|
2904
2952
|
}
|
|
2953
|
+
async findById(id) {
|
|
2954
|
+
const row = this.db.prepare(
|
|
2955
|
+
`SELECT * FROM lt_a2a_api_keys WHERE id = ?`
|
|
2956
|
+
).get(id);
|
|
2957
|
+
return row ? mapRowToRecord(row) : null;
|
|
2958
|
+
}
|
|
2905
2959
|
async list(params) {
|
|
2906
2960
|
const limit = params.limit || 100;
|
|
2907
2961
|
const offset = params.offset || 0;
|
|
@@ -2918,19 +2972,19 @@ var LocalA2AApiKeyStore = class {
|
|
|
2918
2972
|
return rows.map(mapRowToRecord);
|
|
2919
2973
|
}
|
|
2920
2974
|
async create(input) {
|
|
2975
|
+
const id = randomUUID3();
|
|
2921
2976
|
const key = generateApiKey();
|
|
2922
2977
|
const now = nowISO();
|
|
2923
2978
|
this.db.prepare(
|
|
2924
|
-
`INSERT INTO lt_a2a_api_keys (key_value, tenant_id, project_id,
|
|
2925
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
2926
|
-
).run(encrypt3(key), input.tenantId, input.projectId
|
|
2927
|
-
const lastId = this.db.prepare(`SELECT last_insert_rowid() as id`).get();
|
|
2979
|
+
`INSERT INTO lt_a2a_api_keys (id, key_value, tenant_id, project_id, assistant_ids, label, created_at, updated_at)
|
|
2980
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2981
|
+
).run(id, encrypt3(key), input.tenantId, input.projectId, input.assistantIds ? JSON.stringify(input.assistantIds) : null, input.label || null, now, now);
|
|
2928
2982
|
const record = {
|
|
2929
|
-
id
|
|
2983
|
+
id,
|
|
2930
2984
|
key,
|
|
2931
2985
|
tenantId: input.tenantId,
|
|
2932
2986
|
projectId: input.projectId,
|
|
2933
|
-
|
|
2987
|
+
assistantIds: input.assistantIds,
|
|
2934
2988
|
label: input.label,
|
|
2935
2989
|
enabled: true,
|
|
2936
2990
|
createdAt: parseISO(now),
|
|
@@ -2982,8 +3036,8 @@ var LocalA2AApiKeyStore = class {
|
|
|
2982
3036
|
map.set(key, {
|
|
2983
3037
|
key,
|
|
2984
3038
|
tenantId: row.tenant_id,
|
|
2985
|
-
projectId: row.project_id
|
|
2986
|
-
|
|
3039
|
+
projectId: row.project_id,
|
|
3040
|
+
assistantIds: parseAssistantIds(row.assistant_ids)
|
|
2987
3041
|
});
|
|
2988
3042
|
} catch {
|
|
2989
3043
|
}
|
|
@@ -3002,8 +3056,8 @@ function mapRowToRecord(row) {
|
|
|
3002
3056
|
id: row.id,
|
|
3003
3057
|
key,
|
|
3004
3058
|
tenantId: row.tenant_id,
|
|
3005
|
-
projectId: row.project_id
|
|
3006
|
-
|
|
3059
|
+
projectId: row.project_id,
|
|
3060
|
+
assistantIds: parseAssistantIds(row.assistant_ids),
|
|
3007
3061
|
label: row.label || void 0,
|
|
3008
3062
|
enabled: row.enabled === 1,
|
|
3009
3063
|
createdAt: parseISO(row.created_at),
|
|
@@ -3652,6 +3706,12 @@ function mapRowToTask(row) {
|
|
|
3652
3706
|
|
|
3653
3707
|
// src/stores/LocalTaskStore.ts
|
|
3654
3708
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3709
|
+
var MAX_CANONICAL_TASK_TIME = "9999-12-31T23:59:59.999Z";
|
|
3710
|
+
var NEXT_UPDATED_AT_SQL = `updated_at = CASE
|
|
3711
|
+
WHEN strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) = updated_at AND updated_at >= ?
|
|
3712
|
+
THEN strftime('%Y-%m-%dT%H:%M:%fZ', updated_at, '+0.001 seconds')
|
|
3713
|
+
ELSE ?
|
|
3714
|
+
END`;
|
|
3655
3715
|
var DDL20 = `
|
|
3656
3716
|
CREATE TABLE IF NOT EXISTS lt_tasks (
|
|
3657
3717
|
id TEXT NOT NULL,
|
|
@@ -3715,6 +3775,10 @@ function mapRowToTask2(row) {
|
|
|
3715
3775
|
updatedAt: parseISO(row.updated_at)
|
|
3716
3776
|
};
|
|
3717
3777
|
}
|
|
3778
|
+
function nextISO(previous) {
|
|
3779
|
+
const previousMs = new Date(previous).getTime();
|
|
3780
|
+
return new Date(Math.max(Date.now(), previousMs + 1)).toISOString();
|
|
3781
|
+
}
|
|
3718
3782
|
var LocalTaskStore = class {
|
|
3719
3783
|
constructor(db) {
|
|
3720
3784
|
this.db = db;
|
|
@@ -3735,7 +3799,7 @@ var LocalTaskStore = class {
|
|
|
3735
3799
|
}
|
|
3736
3800
|
}
|
|
3737
3801
|
async create(params) {
|
|
3738
|
-
const id = randomUUID5();
|
|
3802
|
+
const id = params.id ?? randomUUID5();
|
|
3739
3803
|
const now = nowISO();
|
|
3740
3804
|
this.db.prepare(
|
|
3741
3805
|
`INSERT INTO lt_tasks (id, tenant_id, owner_type, owner_id, title, description, status, priority, due_date, metadata, parent_id, source_id, context, require_review, dependencies, result, failure_reason, workspace_id, project_id, files, created_at, updated_at)
|
|
@@ -3836,8 +3900,8 @@ var LocalTaskStore = class {
|
|
|
3836
3900
|
const existing = await this.getById(tenantId, id);
|
|
3837
3901
|
if (!existing) return null;
|
|
3838
3902
|
const now = nowISO();
|
|
3839
|
-
const setClauses = [
|
|
3840
|
-
const values = [now];
|
|
3903
|
+
const setClauses = [NEXT_UPDATED_AT_SQL];
|
|
3904
|
+
const values = [now, now];
|
|
3841
3905
|
if (updates.title !== void 0) {
|
|
3842
3906
|
setClauses.push("title = ?");
|
|
3843
3907
|
values.push(updates.title);
|
|
@@ -3872,7 +3936,7 @@ var LocalTaskStore = class {
|
|
|
3872
3936
|
}
|
|
3873
3937
|
if (updates.context !== void 0) {
|
|
3874
3938
|
setClauses.push("context = ?");
|
|
3875
|
-
values.push(JSON.stringify(updates.context));
|
|
3939
|
+
values.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
3876
3940
|
}
|
|
3877
3941
|
if (updates.ownerType !== void 0) {
|
|
3878
3942
|
setClauses.push("owner_type = ?");
|
|
@@ -3911,10 +3975,369 @@ var LocalTaskStore = class {
|
|
|
3911
3975
|
values.push(JSON.stringify(updates.files));
|
|
3912
3976
|
}
|
|
3913
3977
|
if (setClauses.length === 1) return existing;
|
|
3914
|
-
values.push(tenantId, id);
|
|
3915
|
-
this.db.prepare(
|
|
3916
|
-
`UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`
|
|
3978
|
+
values.push(tenantId, id, MAX_CANONICAL_TASK_TIME);
|
|
3979
|
+
const result = this.db.prepare(
|
|
3980
|
+
`UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ?`
|
|
3981
|
+
).run(...values);
|
|
3982
|
+
if (result.changes === 0) return null;
|
|
3983
|
+
return this.getById(tenantId, id);
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Atomically update a task unless its current status is blocked.
|
|
3987
|
+
*
|
|
3988
|
+
* @param tenantId Tenant identifier.
|
|
3989
|
+
* @param id Task identifier.
|
|
3990
|
+
* @param updates Partial task data to update.
|
|
3991
|
+
* @param blockedStatuses Current statuses that prevent the update.
|
|
3992
|
+
* @returns The updated task, or `null` when missing or blocked.
|
|
3993
|
+
*/
|
|
3994
|
+
async updateIfStatusNotIn(tenantId, id, updates, blockedStatuses) {
|
|
3995
|
+
const now = nowISO();
|
|
3996
|
+
const setClauses = [NEXT_UPDATED_AT_SQL];
|
|
3997
|
+
const values = [now, now];
|
|
3998
|
+
if (updates.title !== void 0) {
|
|
3999
|
+
setClauses.push("title = ?");
|
|
4000
|
+
values.push(updates.title);
|
|
4001
|
+
}
|
|
4002
|
+
if (updates.description !== void 0) {
|
|
4003
|
+
setClauses.push("description = ?");
|
|
4004
|
+
values.push(updates.description);
|
|
4005
|
+
}
|
|
4006
|
+
if (updates.status !== void 0) {
|
|
4007
|
+
setClauses.push("status = ?");
|
|
4008
|
+
values.push(updates.status);
|
|
4009
|
+
}
|
|
4010
|
+
if (updates.priority !== void 0) {
|
|
4011
|
+
setClauses.push("priority = ?");
|
|
4012
|
+
values.push(updates.priority);
|
|
4013
|
+
}
|
|
4014
|
+
if (updates.dueDate !== void 0) {
|
|
4015
|
+
setClauses.push("due_date = ?");
|
|
4016
|
+
values.push(updates.dueDate);
|
|
4017
|
+
}
|
|
4018
|
+
if (updates.metadata !== void 0) {
|
|
4019
|
+
setClauses.push("metadata = ?");
|
|
4020
|
+
values.push(JSON.stringify(updates.metadata));
|
|
4021
|
+
}
|
|
4022
|
+
if (updates.parentId !== void 0) {
|
|
4023
|
+
setClauses.push("parent_id = ?");
|
|
4024
|
+
values.push(updates.parentId);
|
|
4025
|
+
}
|
|
4026
|
+
if (updates.sourceId !== void 0) {
|
|
4027
|
+
setClauses.push("source_id = ?");
|
|
4028
|
+
values.push(updates.sourceId);
|
|
4029
|
+
}
|
|
4030
|
+
if (updates.context !== void 0) {
|
|
4031
|
+
setClauses.push("context = ?");
|
|
4032
|
+
values.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
4033
|
+
}
|
|
4034
|
+
if (updates.ownerType !== void 0) {
|
|
4035
|
+
setClauses.push("owner_type = ?");
|
|
4036
|
+
values.push(updates.ownerType);
|
|
4037
|
+
}
|
|
4038
|
+
if (updates.ownerId !== void 0) {
|
|
4039
|
+
setClauses.push("owner_id = ?");
|
|
4040
|
+
values.push(updates.ownerId);
|
|
4041
|
+
}
|
|
4042
|
+
if (updates.requireReview !== void 0) {
|
|
4043
|
+
setClauses.push("require_review = ?");
|
|
4044
|
+
values.push(updates.requireReview ? 1 : 0);
|
|
4045
|
+
}
|
|
4046
|
+
if (updates.dependencies !== void 0) {
|
|
4047
|
+
setClauses.push("dependencies = ?");
|
|
4048
|
+
values.push(JSON.stringify(updates.dependencies));
|
|
4049
|
+
}
|
|
4050
|
+
if (updates.result !== void 0) {
|
|
4051
|
+
setClauses.push("result = ?");
|
|
4052
|
+
values.push(updates.result);
|
|
4053
|
+
}
|
|
4054
|
+
if (updates.failureReason !== void 0) {
|
|
4055
|
+
setClauses.push("failure_reason = ?");
|
|
4056
|
+
values.push(updates.failureReason);
|
|
4057
|
+
}
|
|
4058
|
+
if (updates.workspaceId !== void 0) {
|
|
4059
|
+
setClauses.push("workspace_id = ?");
|
|
4060
|
+
values.push(updates.workspaceId);
|
|
4061
|
+
}
|
|
4062
|
+
if (updates.projectId !== void 0) {
|
|
4063
|
+
setClauses.push("project_id = ?");
|
|
4064
|
+
values.push(updates.projectId);
|
|
4065
|
+
}
|
|
4066
|
+
if (updates.files !== void 0) {
|
|
4067
|
+
setClauses.push("files = ?");
|
|
4068
|
+
values.push(JSON.stringify(updates.files));
|
|
4069
|
+
}
|
|
4070
|
+
const statusClause = blockedStatuses.length > 0 ? ` AND status NOT IN (${blockedStatuses.map(() => "?").join(", ")})` : "";
|
|
4071
|
+
values.push(tenantId, id, MAX_CANONICAL_TASK_TIME, ...blockedStatuses);
|
|
4072
|
+
const result = this.db.prepare(
|
|
4073
|
+
`UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ?${statusClause}`
|
|
4074
|
+
).run(...values);
|
|
4075
|
+
if (result.changes === 0) return null;
|
|
4076
|
+
return this.getById(tenantId, id);
|
|
4077
|
+
}
|
|
4078
|
+
/** Atomically update a task only when its current status is expected. */
|
|
4079
|
+
async updateIfStatusIn(tenantId, id, updates, expectedStatuses) {
|
|
4080
|
+
if (expectedStatuses.length === 0) return null;
|
|
4081
|
+
const now = nowISO();
|
|
4082
|
+
const setClauses = [NEXT_UPDATED_AT_SQL];
|
|
4083
|
+
const values = [now, now];
|
|
4084
|
+
if (updates.title !== void 0) {
|
|
4085
|
+
setClauses.push("title = ?");
|
|
4086
|
+
values.push(updates.title);
|
|
4087
|
+
}
|
|
4088
|
+
if (updates.description !== void 0) {
|
|
4089
|
+
setClauses.push("description = ?");
|
|
4090
|
+
values.push(updates.description);
|
|
4091
|
+
}
|
|
4092
|
+
if (updates.status !== void 0) {
|
|
4093
|
+
setClauses.push("status = ?");
|
|
4094
|
+
values.push(updates.status);
|
|
4095
|
+
}
|
|
4096
|
+
if (updates.priority !== void 0) {
|
|
4097
|
+
setClauses.push("priority = ?");
|
|
4098
|
+
values.push(updates.priority);
|
|
4099
|
+
}
|
|
4100
|
+
if (updates.dueDate !== void 0) {
|
|
4101
|
+
setClauses.push("due_date = ?");
|
|
4102
|
+
values.push(updates.dueDate);
|
|
4103
|
+
}
|
|
4104
|
+
if (updates.metadata !== void 0) {
|
|
4105
|
+
setClauses.push("metadata = ?");
|
|
4106
|
+
values.push(JSON.stringify(updates.metadata));
|
|
4107
|
+
}
|
|
4108
|
+
if (updates.parentId !== void 0) {
|
|
4109
|
+
setClauses.push("parent_id = ?");
|
|
4110
|
+
values.push(updates.parentId);
|
|
4111
|
+
}
|
|
4112
|
+
if (updates.sourceId !== void 0) {
|
|
4113
|
+
setClauses.push("source_id = ?");
|
|
4114
|
+
values.push(updates.sourceId);
|
|
4115
|
+
}
|
|
4116
|
+
if (updates.context !== void 0) {
|
|
4117
|
+
setClauses.push("context = ?");
|
|
4118
|
+
values.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
4119
|
+
}
|
|
4120
|
+
if (updates.ownerType !== void 0) {
|
|
4121
|
+
setClauses.push("owner_type = ?");
|
|
4122
|
+
values.push(updates.ownerType);
|
|
4123
|
+
}
|
|
4124
|
+
if (updates.ownerId !== void 0) {
|
|
4125
|
+
setClauses.push("owner_id = ?");
|
|
4126
|
+
values.push(updates.ownerId);
|
|
4127
|
+
}
|
|
4128
|
+
if (updates.requireReview !== void 0) {
|
|
4129
|
+
setClauses.push("require_review = ?");
|
|
4130
|
+
values.push(updates.requireReview ? 1 : 0);
|
|
4131
|
+
}
|
|
4132
|
+
if (updates.dependencies !== void 0) {
|
|
4133
|
+
setClauses.push("dependencies = ?");
|
|
4134
|
+
values.push(JSON.stringify(updates.dependencies));
|
|
4135
|
+
}
|
|
4136
|
+
if (updates.result !== void 0) {
|
|
4137
|
+
setClauses.push("result = ?");
|
|
4138
|
+
values.push(updates.result);
|
|
4139
|
+
}
|
|
4140
|
+
if (updates.failureReason !== void 0) {
|
|
4141
|
+
setClauses.push("failure_reason = ?");
|
|
4142
|
+
values.push(updates.failureReason);
|
|
4143
|
+
}
|
|
4144
|
+
if (updates.workspaceId !== void 0) {
|
|
4145
|
+
setClauses.push("workspace_id = ?");
|
|
4146
|
+
values.push(updates.workspaceId);
|
|
4147
|
+
}
|
|
4148
|
+
if (updates.projectId !== void 0) {
|
|
4149
|
+
setClauses.push("project_id = ?");
|
|
4150
|
+
values.push(updates.projectId);
|
|
4151
|
+
}
|
|
4152
|
+
if (updates.files !== void 0) {
|
|
4153
|
+
setClauses.push("files = ?");
|
|
4154
|
+
values.push(JSON.stringify(updates.files));
|
|
4155
|
+
}
|
|
4156
|
+
values.push(tenantId, id, MAX_CANONICAL_TASK_TIME, ...expectedStatuses);
|
|
4157
|
+
const result = this.db.prepare(
|
|
4158
|
+
`UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ? AND status IN (${expectedStatuses.map(() => "?").join(", ")})`
|
|
4159
|
+
).run(...values);
|
|
4160
|
+
if (result.changes === 0) return null;
|
|
4161
|
+
return this.getById(tenantId, id);
|
|
4162
|
+
}
|
|
4163
|
+
/** Atomically update a task only when status and updatedAt match a read snapshot. */
|
|
4164
|
+
async updateIfStatusAndUpdatedAt(tenantId, id, updates, expectedStatuses, expectedUpdatedAt) {
|
|
4165
|
+
if (expectedStatuses.length === 0) return null;
|
|
4166
|
+
const expectedIso = new Date(expectedUpdatedAt).toISOString();
|
|
4167
|
+
if (expectedIso === MAX_CANONICAL_TASK_TIME) return null;
|
|
4168
|
+
const setClauses = ["updated_at = ?"];
|
|
4169
|
+
const values = [nextISO(expectedIso)];
|
|
4170
|
+
if (updates.title !== void 0) {
|
|
4171
|
+
setClauses.push("title = ?");
|
|
4172
|
+
values.push(updates.title);
|
|
4173
|
+
}
|
|
4174
|
+
if (updates.description !== void 0) {
|
|
4175
|
+
setClauses.push("description = ?");
|
|
4176
|
+
values.push(updates.description);
|
|
4177
|
+
}
|
|
4178
|
+
if (updates.status !== void 0) {
|
|
4179
|
+
setClauses.push("status = ?");
|
|
4180
|
+
values.push(updates.status);
|
|
4181
|
+
}
|
|
4182
|
+
if (updates.priority !== void 0) {
|
|
4183
|
+
setClauses.push("priority = ?");
|
|
4184
|
+
values.push(updates.priority);
|
|
4185
|
+
}
|
|
4186
|
+
if (updates.dueDate !== void 0) {
|
|
4187
|
+
setClauses.push("due_date = ?");
|
|
4188
|
+
values.push(updates.dueDate);
|
|
4189
|
+
}
|
|
4190
|
+
if (updates.metadata !== void 0) {
|
|
4191
|
+
setClauses.push("metadata = ?");
|
|
4192
|
+
values.push(JSON.stringify(updates.metadata));
|
|
4193
|
+
}
|
|
4194
|
+
if (updates.parentId !== void 0) {
|
|
4195
|
+
setClauses.push("parent_id = ?");
|
|
4196
|
+
values.push(updates.parentId);
|
|
4197
|
+
}
|
|
4198
|
+
if (updates.sourceId !== void 0) {
|
|
4199
|
+
setClauses.push("source_id = ?");
|
|
4200
|
+
values.push(updates.sourceId);
|
|
4201
|
+
}
|
|
4202
|
+
if (updates.context !== void 0) {
|
|
4203
|
+
setClauses.push("context = ?");
|
|
4204
|
+
values.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
4205
|
+
}
|
|
4206
|
+
if (updates.ownerType !== void 0) {
|
|
4207
|
+
setClauses.push("owner_type = ?");
|
|
4208
|
+
values.push(updates.ownerType);
|
|
4209
|
+
}
|
|
4210
|
+
if (updates.ownerId !== void 0) {
|
|
4211
|
+
setClauses.push("owner_id = ?");
|
|
4212
|
+
values.push(updates.ownerId);
|
|
4213
|
+
}
|
|
4214
|
+
if (updates.requireReview !== void 0) {
|
|
4215
|
+
setClauses.push("require_review = ?");
|
|
4216
|
+
values.push(updates.requireReview ? 1 : 0);
|
|
4217
|
+
}
|
|
4218
|
+
if (updates.dependencies !== void 0) {
|
|
4219
|
+
setClauses.push("dependencies = ?");
|
|
4220
|
+
values.push(JSON.stringify(updates.dependencies));
|
|
4221
|
+
}
|
|
4222
|
+
if (updates.result !== void 0) {
|
|
4223
|
+
setClauses.push("result = ?");
|
|
4224
|
+
values.push(updates.result);
|
|
4225
|
+
}
|
|
4226
|
+
if (updates.failureReason !== void 0) {
|
|
4227
|
+
setClauses.push("failure_reason = ?");
|
|
4228
|
+
values.push(updates.failureReason);
|
|
4229
|
+
}
|
|
4230
|
+
if (updates.workspaceId !== void 0) {
|
|
4231
|
+
setClauses.push("workspace_id = ?");
|
|
4232
|
+
values.push(updates.workspaceId);
|
|
4233
|
+
}
|
|
4234
|
+
if (updates.projectId !== void 0) {
|
|
4235
|
+
setClauses.push("project_id = ?");
|
|
4236
|
+
values.push(updates.projectId);
|
|
4237
|
+
}
|
|
4238
|
+
if (updates.files !== void 0) {
|
|
4239
|
+
setClauses.push("files = ?");
|
|
4240
|
+
values.push(JSON.stringify(updates.files));
|
|
4241
|
+
}
|
|
4242
|
+
values.push(tenantId, id, ...expectedStatuses, expectedIso);
|
|
4243
|
+
const result = this.db.prepare(
|
|
4244
|
+
`UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND status IN (${expectedStatuses.map(() => "?").join(", ")}) AND updated_at = ?`
|
|
4245
|
+
).run(...values);
|
|
4246
|
+
if (result.changes === 0) return null;
|
|
4247
|
+
return this.getById(tenantId, id);
|
|
4248
|
+
}
|
|
4249
|
+
/** Atomically update a child only when both child and parent snapshots match. */
|
|
4250
|
+
async updateIfStatusUpdatedAtAndParentUpdatedAt(tenantId, id, updates, expectedStatuses, expectedUpdatedAt, parentId, expectedParentUpdatedAt) {
|
|
4251
|
+
if (expectedStatuses.length === 0) return null;
|
|
4252
|
+
const expectedIso = new Date(expectedUpdatedAt).toISOString();
|
|
4253
|
+
if (expectedIso === MAX_CANONICAL_TASK_TIME) return null;
|
|
4254
|
+
const parentExpectedIso = new Date(expectedParentUpdatedAt).toISOString();
|
|
4255
|
+
const setClauses = ["updated_at = ?"];
|
|
4256
|
+
const values = [nextISO(expectedIso)];
|
|
4257
|
+
if (updates.title !== void 0) {
|
|
4258
|
+
setClauses.push("title = ?");
|
|
4259
|
+
values.push(updates.title);
|
|
4260
|
+
}
|
|
4261
|
+
if (updates.description !== void 0) {
|
|
4262
|
+
setClauses.push("description = ?");
|
|
4263
|
+
values.push(updates.description);
|
|
4264
|
+
}
|
|
4265
|
+
if (updates.status !== void 0) {
|
|
4266
|
+
setClauses.push("status = ?");
|
|
4267
|
+
values.push(updates.status);
|
|
4268
|
+
}
|
|
4269
|
+
if (updates.priority !== void 0) {
|
|
4270
|
+
setClauses.push("priority = ?");
|
|
4271
|
+
values.push(updates.priority);
|
|
4272
|
+
}
|
|
4273
|
+
if (updates.dueDate !== void 0) {
|
|
4274
|
+
setClauses.push("due_date = ?");
|
|
4275
|
+
values.push(updates.dueDate);
|
|
4276
|
+
}
|
|
4277
|
+
if (updates.metadata !== void 0) {
|
|
4278
|
+
setClauses.push("metadata = ?");
|
|
4279
|
+
values.push(JSON.stringify(updates.metadata));
|
|
4280
|
+
}
|
|
4281
|
+
if (updates.parentId !== void 0) {
|
|
4282
|
+
setClauses.push("parent_id = ?");
|
|
4283
|
+
values.push(updates.parentId);
|
|
4284
|
+
}
|
|
4285
|
+
if (updates.sourceId !== void 0) {
|
|
4286
|
+
setClauses.push("source_id = ?");
|
|
4287
|
+
values.push(updates.sourceId);
|
|
4288
|
+
}
|
|
4289
|
+
if (updates.context !== void 0) {
|
|
4290
|
+
setClauses.push("context = ?");
|
|
4291
|
+
values.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
4292
|
+
}
|
|
4293
|
+
if (updates.ownerType !== void 0) {
|
|
4294
|
+
setClauses.push("owner_type = ?");
|
|
4295
|
+
values.push(updates.ownerType);
|
|
4296
|
+
}
|
|
4297
|
+
if (updates.ownerId !== void 0) {
|
|
4298
|
+
setClauses.push("owner_id = ?");
|
|
4299
|
+
values.push(updates.ownerId);
|
|
4300
|
+
}
|
|
4301
|
+
if (updates.requireReview !== void 0) {
|
|
4302
|
+
setClauses.push("require_review = ?");
|
|
4303
|
+
values.push(updates.requireReview ? 1 : 0);
|
|
4304
|
+
}
|
|
4305
|
+
if (updates.dependencies !== void 0) {
|
|
4306
|
+
setClauses.push("dependencies = ?");
|
|
4307
|
+
values.push(JSON.stringify(updates.dependencies));
|
|
4308
|
+
}
|
|
4309
|
+
if (updates.result !== void 0) {
|
|
4310
|
+
setClauses.push("result = ?");
|
|
4311
|
+
values.push(updates.result);
|
|
4312
|
+
}
|
|
4313
|
+
if (updates.failureReason !== void 0) {
|
|
4314
|
+
setClauses.push("failure_reason = ?");
|
|
4315
|
+
values.push(updates.failureReason);
|
|
4316
|
+
}
|
|
4317
|
+
if (updates.workspaceId !== void 0) {
|
|
4318
|
+
setClauses.push("workspace_id = ?");
|
|
4319
|
+
values.push(updates.workspaceId);
|
|
4320
|
+
}
|
|
4321
|
+
if (updates.projectId !== void 0) {
|
|
4322
|
+
setClauses.push("project_id = ?");
|
|
4323
|
+
values.push(updates.projectId);
|
|
4324
|
+
}
|
|
4325
|
+
if (updates.files !== void 0) {
|
|
4326
|
+
setClauses.push("files = ?");
|
|
4327
|
+
values.push(JSON.stringify(updates.files));
|
|
4328
|
+
}
|
|
4329
|
+
values.push(tenantId, id, ...expectedStatuses, expectedIso, parentId, parentExpectedIso);
|
|
4330
|
+
const result = this.db.prepare(
|
|
4331
|
+
`UPDATE lt_tasks AS child SET ${setClauses.join(", ")}
|
|
4332
|
+
WHERE tenant_id = ? AND id = ?
|
|
4333
|
+
AND status IN (${expectedStatuses.map(() => "?").join(", ")})
|
|
4334
|
+
AND updated_at = ?
|
|
4335
|
+
AND EXISTS (
|
|
4336
|
+
SELECT 1 FROM lt_tasks AS parent
|
|
4337
|
+
WHERE parent.tenant_id = child.tenant_id AND parent.id = ? AND parent.updated_at = ?
|
|
4338
|
+
)`
|
|
3917
4339
|
).run(...values);
|
|
4340
|
+
if (result.changes === 0) return null;
|
|
3918
4341
|
return this.getById(tenantId, id);
|
|
3919
4342
|
}
|
|
3920
4343
|
async delete(tenantId, id) {
|
|
@@ -3957,6 +4380,7 @@ function mapRowToWorkItem(row) {
|
|
|
3957
4380
|
attempt: row.attempt ?? void 0,
|
|
3958
4381
|
workspaceId: row.workspace_id ?? void 0,
|
|
3959
4382
|
projectId: row.project_id ?? void 0,
|
|
4383
|
+
eventKey: row.event_key ?? void 0,
|
|
3960
4384
|
createdAt: parseISO(row.created_at)
|
|
3961
4385
|
};
|
|
3962
4386
|
}
|
|
@@ -3964,8 +4388,18 @@ var LocalTaskWorkItemStore = class {
|
|
|
3964
4388
|
constructor(db) {
|
|
3965
4389
|
this.db = db;
|
|
3966
4390
|
ensureTable(db, DDL21);
|
|
3967
|
-
this.
|
|
3968
|
-
this.
|
|
4391
|
+
this.ensureColumn("lt_task_work_items", "workspace_id", "workspace_id TEXT");
|
|
4392
|
+
this.ensureColumn("lt_task_work_items", "project_id", "project_id TEXT");
|
|
4393
|
+
this.ensureColumn("lt_task_work_items", "event_key", "event_key TEXT");
|
|
4394
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
|
|
4395
|
+
ON lt_task_work_items (tenant_id, task_id, event_key);`);
|
|
4396
|
+
}
|
|
4397
|
+
/** Add a column if it does not exist (SQLite version compatible). */
|
|
4398
|
+
ensureColumn(table, column, ddl) {
|
|
4399
|
+
const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
4400
|
+
if (!cols.some((c) => c.name === column)) {
|
|
4401
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
4402
|
+
}
|
|
3969
4403
|
}
|
|
3970
4404
|
async create(params) {
|
|
3971
4405
|
const id = uuid2();
|
|
@@ -3992,6 +4426,39 @@ var LocalTaskWorkItemStore = class {
|
|
|
3992
4426
|
).get(params.tenantId, id);
|
|
3993
4427
|
return mapRowToWorkItem(row);
|
|
3994
4428
|
}
|
|
4429
|
+
/** Find an event by its tenant- and task-scoped key without pagination. */
|
|
4430
|
+
async findByEventKey(tenantId, taskId, eventKey) {
|
|
4431
|
+
const row = this.db.prepare(
|
|
4432
|
+
`SELECT * FROM lt_task_work_items WHERE tenant_id = ? AND task_id = ? AND event_key = ?`
|
|
4433
|
+
).get(tenantId, taskId, eventKey);
|
|
4434
|
+
return row ? mapRowToWorkItem(row) : null;
|
|
4435
|
+
}
|
|
4436
|
+
/** Atomically return an existing event or create it once. */
|
|
4437
|
+
async createIfAbsentByEventKey(params) {
|
|
4438
|
+
const id = uuid2();
|
|
4439
|
+
this.db.prepare(
|
|
4440
|
+
`INSERT INTO lt_task_work_items (id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id, event_key, created_at)
|
|
4441
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4442
|
+
ON CONFLICT (tenant_id, task_id, event_key) DO NOTHING`
|
|
4443
|
+
).run(
|
|
4444
|
+
id,
|
|
4445
|
+
params.tenantId,
|
|
4446
|
+
params.taskId,
|
|
4447
|
+
params.action,
|
|
4448
|
+
params.actor,
|
|
4449
|
+
params.threadId || null,
|
|
4450
|
+
params.summary || null,
|
|
4451
|
+
params.detail ? JSON.stringify(params.detail) : null,
|
|
4452
|
+
params.attempt ?? null,
|
|
4453
|
+
params.workspaceId || null,
|
|
4454
|
+
params.projectId || null,
|
|
4455
|
+
params.eventKey,
|
|
4456
|
+
nowISO()
|
|
4457
|
+
);
|
|
4458
|
+
const item = await this.findByEventKey(params.tenantId, params.taskId, params.eventKey);
|
|
4459
|
+
if (!item) throw new Error("Task work item insert did not return a row");
|
|
4460
|
+
return item;
|
|
4461
|
+
}
|
|
3995
4462
|
async list(filter) {
|
|
3996
4463
|
const conditions = ["tenant_id = ?", "task_id = ?"];
|
|
3997
4464
|
const params = [filter.tenantId, filter.taskId];
|
|
@@ -3999,11 +4466,20 @@ var LocalTaskWorkItemStore = class {
|
|
|
3999
4466
|
conditions.push("action = ?");
|
|
4000
4467
|
params.push(filter.action);
|
|
4001
4468
|
}
|
|
4469
|
+
if (filter.workspaceId) {
|
|
4470
|
+
conditions.push("workspace_id = ?");
|
|
4471
|
+
params.push(filter.workspaceId);
|
|
4472
|
+
}
|
|
4473
|
+
if (filter.projectId) {
|
|
4474
|
+
conditions.push("project_id = ?");
|
|
4475
|
+
params.push(filter.projectId);
|
|
4476
|
+
}
|
|
4002
4477
|
const where = conditions.join(" AND ");
|
|
4003
4478
|
const limit = filter.limit || 100;
|
|
4004
4479
|
const offset = filter.offset || 0;
|
|
4480
|
+
const order = filter.order === "asc" ? "ASC" : "DESC";
|
|
4005
4481
|
const rows = this.db.prepare(
|
|
4006
|
-
`SELECT * FROM lt_task_work_items WHERE ${where} ORDER BY created_at
|
|
4482
|
+
`SELECT * FROM lt_task_work_items WHERE ${where} ORDER BY created_at ${order}, id ${order} LIMIT ? OFFSET ?`
|
|
4007
4483
|
).all(...params, limit, offset);
|
|
4008
4484
|
return rows.map(mapRowToWorkItem);
|
|
4009
4485
|
}
|