@hasna/todos 0.11.75 → 0.11.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cli/cloud-router.d.ts +90 -0
- package/dist/cli/cloud-router.d.ts.map +1 -0
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/storage-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +22 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +72556 -62360
- package/dist/contracts.js +314 -14
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +1160 -236
- package/dist/lib/shadow-status.d.ts +38 -0
- package/dist/lib/shadow-status.d.ts.map +1 -0
- package/dist/mcp/index.js +10378 -341
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-workflow-tools.d.ts.map +1 -1
- package/dist/registry.js +314 -14
- package/dist/release-provenance.json +3 -3
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +135 -0
- package/dist/sdk/v1.generated.d.ts +124 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +50 -0
- package/dist/server/cloud.d.ts.map +1 -0
- package/dist/server/index.js +12073 -2013
- package/dist/server/openapi.d.ts +628 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts +19 -0
- package/dist/server/v1.d.ts.map +1 -0
- package/dist/storage/cloud-client.d.ts +30 -0
- package/dist/storage/cloud-client.d.ts.map +1 -0
- package/dist/storage/config.d.ts +12 -0
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/factory.d.ts +2 -0
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/index.d.ts +8 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +1 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow-outbox-schema.d.ts +21 -0
- package/dist/storage/shadow-outbox-schema.d.ts.map +1 -0
- package/dist/storage/shadow-outbox.d.ts +125 -0
- package/dist/storage/shadow-outbox.d.ts.map +1 -0
- package/dist/storage/shadow-runtime.d.ts +29 -0
- package/dist/storage/shadow-runtime.d.ts.map +1 -0
- package/dist/storage/shadow.d.ts +118 -0
- package/dist/storage/shadow.d.ts.map +1 -0
- package/dist/storage.d.ts +2 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1159 -220
- package/package.json +6 -2
- package/vendor/hasna-contracts-0.5.1.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -2643,6 +2643,294 @@ var init_machines = __esm(() => {
|
|
|
2643
2643
|
];
|
|
2644
2644
|
});
|
|
2645
2645
|
|
|
2646
|
+
// src/storage/config.ts
|
|
2647
|
+
var exports_config = {};
|
|
2648
|
+
__export(exports_config, {
|
|
2649
|
+
parseStorageMode: () => parseStorageMode,
|
|
2650
|
+
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
2651
|
+
loadStorageConfig: () => loadStorageConfig,
|
|
2652
|
+
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
2653
|
+
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
2654
|
+
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
2655
|
+
getTodosStorageMode: () => getTodosStorageMode,
|
|
2656
|
+
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
2657
|
+
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
2658
|
+
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
2659
|
+
getStorageMode: () => getStorageMode,
|
|
2660
|
+
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
2661
|
+
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
2662
|
+
getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
|
|
2663
|
+
assertTodosShadowConfig: () => assertTodosShadowConfig,
|
|
2664
|
+
assertTodosRemoteStorageConfig: () => assertTodosRemoteStorageConfig,
|
|
2665
|
+
TODOS_STORAGE_TABLES: () => TODOS_STORAGE_TABLES,
|
|
2666
|
+
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
2667
|
+
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
2668
|
+
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
2669
|
+
CANONICAL_TODOS_RDS_RUNTIME_PATH: () => CANONICAL_TODOS_RDS_RUNTIME_PATH,
|
|
2670
|
+
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
2671
|
+
CANONICAL_TODOS_RDS_CLUSTER: () => CANONICAL_TODOS_RDS_CLUSTER
|
|
2672
|
+
});
|
|
2673
|
+
function getCanonicalTodosRdsConfig() {
|
|
2674
|
+
return {
|
|
2675
|
+
cluster: CANONICAL_TODOS_RDS_CLUSTER,
|
|
2676
|
+
database: CANONICAL_TODOS_RDS_DATABASE,
|
|
2677
|
+
runtimeSecretPath: CANONICAL_TODOS_RDS_RUNTIME_PATH,
|
|
2678
|
+
primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
|
|
2679
|
+
fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
function loadTodosStorageConfig(env = process.env) {
|
|
2683
|
+
const mode = getTodosStorageMode(env);
|
|
2684
|
+
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
2685
|
+
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
2686
|
+
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
2687
|
+
const region = readStorageEnv(env, "awsRegion").value;
|
|
2688
|
+
const endpoint = readStorageEnv(env, "s3Endpoint").value;
|
|
2689
|
+
const schema = readStorageEnv(env, "databaseSchema").value;
|
|
2690
|
+
return {
|
|
2691
|
+
service: "todos",
|
|
2692
|
+
mode,
|
|
2693
|
+
...databaseUrl ? {
|
|
2694
|
+
database: {
|
|
2695
|
+
provider: "postgres",
|
|
2696
|
+
url: databaseUrl,
|
|
2697
|
+
ssl: parseBoolean(readStorageEnv(env, "databaseSsl").value, true),
|
|
2698
|
+
...schema ? { schema } : {}
|
|
2699
|
+
}
|
|
2700
|
+
} : {},
|
|
2701
|
+
...bucket ? {
|
|
2702
|
+
objectStorage: {
|
|
2703
|
+
provider: "s3",
|
|
2704
|
+
bucket,
|
|
2705
|
+
prefix,
|
|
2706
|
+
...region ? { region } : {},
|
|
2707
|
+
...endpoint ? { endpoint } : {},
|
|
2708
|
+
forcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value, false)
|
|
2709
|
+
}
|
|
2710
|
+
} : {},
|
|
2711
|
+
sync: {
|
|
2712
|
+
batchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value, 500),
|
|
2713
|
+
dryRun: parseBoolean(readStorageEnv(env, "syncDryRun").value, false)
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function loadStorageConfig(env = process.env) {
|
|
2718
|
+
return loadTodosStorageConfig(env);
|
|
2719
|
+
}
|
|
2720
|
+
function isTodosRemoteStorageEnabled(config) {
|
|
2721
|
+
return config.mode === "remote" || config.mode === "hybrid";
|
|
2722
|
+
}
|
|
2723
|
+
function assertTodosRemoteStorageConfig(config) {
|
|
2724
|
+
if (!isTodosRemoteStorageEnabled(config))
|
|
2725
|
+
return;
|
|
2726
|
+
if (!config.database?.url) {
|
|
2727
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
function parseStorageMode(value) {
|
|
2731
|
+
const normalized = clean(value)?.toLowerCase();
|
|
2732
|
+
if (!normalized)
|
|
2733
|
+
return "local";
|
|
2734
|
+
if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
|
|
2735
|
+
return normalized;
|
|
2736
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
|
|
2737
|
+
}
|
|
2738
|
+
function getTodosStorageMode(env = process.env) {
|
|
2739
|
+
return parseStorageMode(readStorageEnv(env, "mode").value);
|
|
2740
|
+
}
|
|
2741
|
+
function getStorageMode(env = process.env) {
|
|
2742
|
+
return getTodosStorageMode(env);
|
|
2743
|
+
}
|
|
2744
|
+
function isTodosShadowEnabled(env = process.env) {
|
|
2745
|
+
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
2746
|
+
}
|
|
2747
|
+
function getTodosStorageShadowEnvName(env = process.env) {
|
|
2748
|
+
return readStorageEnv(env, "shadow").name;
|
|
2749
|
+
}
|
|
2750
|
+
function assertTodosShadowConfig(config, env = process.env) {
|
|
2751
|
+
if (!isTodosShadowEnabled(env))
|
|
2752
|
+
return;
|
|
2753
|
+
if (config.mode !== "local") {
|
|
2754
|
+
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires ${TODOS_STORAGE_ENV.mode}=local (got ${config.mode})`);
|
|
2755
|
+
}
|
|
2756
|
+
if (!config.database?.url) {
|
|
2757
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
function getTodosStorageDatabaseEnv(env = process.env) {
|
|
2761
|
+
return readStorageEnv(env, "databaseUrl").name;
|
|
2762
|
+
}
|
|
2763
|
+
function getStorageDatabaseEnv(env = process.env) {
|
|
2764
|
+
return getTodosStorageDatabaseEnv(env);
|
|
2765
|
+
}
|
|
2766
|
+
function getTodosStorageDatabaseUrl(env = process.env) {
|
|
2767
|
+
return readStorageEnv(env, "databaseUrl").value;
|
|
2768
|
+
}
|
|
2769
|
+
function getStorageDatabaseUrl(env = process.env) {
|
|
2770
|
+
return getTodosStorageDatabaseUrl(env);
|
|
2771
|
+
}
|
|
2772
|
+
function getTodosStorageEnvName(env, key) {
|
|
2773
|
+
return readStorageEnv(env, key).name;
|
|
2774
|
+
}
|
|
2775
|
+
function readStorageEnv(env, key) {
|
|
2776
|
+
const primaryName = TODOS_STORAGE_ENV[key];
|
|
2777
|
+
const primaryValue = clean(env[primaryName]);
|
|
2778
|
+
if (primaryValue !== undefined)
|
|
2779
|
+
return { name: primaryName, value: primaryValue };
|
|
2780
|
+
const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
|
|
2781
|
+
const fallbackValue = clean(env[fallbackName]);
|
|
2782
|
+
if (fallbackValue !== undefined)
|
|
2783
|
+
return { name: fallbackName, value: fallbackValue };
|
|
2784
|
+
return { name: primaryName };
|
|
2785
|
+
}
|
|
2786
|
+
function clean(value) {
|
|
2787
|
+
const trimmed = value?.trim();
|
|
2788
|
+
return trimmed ? trimmed : undefined;
|
|
2789
|
+
}
|
|
2790
|
+
function parseBoolean(value, fallback) {
|
|
2791
|
+
const normalized = clean(value)?.toLowerCase();
|
|
2792
|
+
if (!normalized)
|
|
2793
|
+
return fallback;
|
|
2794
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
2795
|
+
return true;
|
|
2796
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
2797
|
+
return false;
|
|
2798
|
+
throw new Error(`Expected boolean env value, got ${value}`);
|
|
2799
|
+
}
|
|
2800
|
+
function parsePositiveInteger(value, fallback) {
|
|
2801
|
+
const normalized = clean(value);
|
|
2802
|
+
if (!normalized)
|
|
2803
|
+
return fallback;
|
|
2804
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
2805
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
2806
|
+
throw new Error(`${TODOS_STORAGE_ENV.syncBatchSize} must be a positive integer`);
|
|
2807
|
+
}
|
|
2808
|
+
return parsed;
|
|
2809
|
+
}
|
|
2810
|
+
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres", CANONICAL_TODOS_RDS_DATABASE = "todos", CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
|
|
2811
|
+
var init_config = __esm(() => {
|
|
2812
|
+
TODOS_STORAGE_TABLES = [
|
|
2813
|
+
"todos_sync_records",
|
|
2814
|
+
"todos_sync_cursors"
|
|
2815
|
+
];
|
|
2816
|
+
STORAGE_TABLES = TODOS_STORAGE_TABLES;
|
|
2817
|
+
TODOS_STORAGE_ENV = {
|
|
2818
|
+
mode: "HASNA_TODOS_STORAGE_MODE",
|
|
2819
|
+
shadow: "HASNA_TODOS_SHADOW",
|
|
2820
|
+
databaseUrl: "HASNA_TODOS_DATABASE_URL",
|
|
2821
|
+
databaseSsl: "HASNA_TODOS_DATABASE_SSL",
|
|
2822
|
+
databaseSchema: "HASNA_TODOS_DATABASE_SCHEMA",
|
|
2823
|
+
s3Bucket: "HASNA_TODOS_S3_BUCKET",
|
|
2824
|
+
s3Prefix: "HASNA_TODOS_S3_PREFIX",
|
|
2825
|
+
awsRegion: "HASNA_TODOS_AWS_REGION",
|
|
2826
|
+
s3Endpoint: "HASNA_TODOS_S3_ENDPOINT",
|
|
2827
|
+
s3ForcePathStyle: "HASNA_TODOS_S3_FORCE_PATH_STYLE",
|
|
2828
|
+
s3AccessKeyId: "HASNA_TODOS_S3_ACCESS_KEY_ID",
|
|
2829
|
+
s3SecretAccessKey: "HASNA_TODOS_S3_SECRET_ACCESS_KEY",
|
|
2830
|
+
s3SessionToken: "HASNA_TODOS_S3_SESSION_TOKEN",
|
|
2831
|
+
syncBatchSize: "HASNA_TODOS_SYNC_BATCH_SIZE",
|
|
2832
|
+
syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
|
|
2833
|
+
};
|
|
2834
|
+
TODOS_STORAGE_FALLBACK_ENV = {
|
|
2835
|
+
mode: "TODOS_STORAGE_MODE",
|
|
2836
|
+
shadow: "TODOS_SHADOW",
|
|
2837
|
+
databaseUrl: "TODOS_DATABASE_URL",
|
|
2838
|
+
databaseSsl: "TODOS_DATABASE_SSL",
|
|
2839
|
+
databaseSchema: "TODOS_DATABASE_SCHEMA",
|
|
2840
|
+
s3Bucket: "TODOS_S3_BUCKET",
|
|
2841
|
+
s3Prefix: "TODOS_S3_PREFIX",
|
|
2842
|
+
awsRegion: "TODOS_AWS_REGION",
|
|
2843
|
+
s3Endpoint: "TODOS_S3_ENDPOINT",
|
|
2844
|
+
s3ForcePathStyle: "TODOS_S3_FORCE_PATH_STYLE",
|
|
2845
|
+
s3AccessKeyId: "TODOS_S3_ACCESS_KEY_ID",
|
|
2846
|
+
s3SecretAccessKey: "TODOS_S3_SECRET_ACCESS_KEY",
|
|
2847
|
+
s3SessionToken: "TODOS_S3_SESSION_TOKEN",
|
|
2848
|
+
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
2849
|
+
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
2850
|
+
};
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
// src/storage/shadow-outbox-schema.ts
|
|
2854
|
+
var exports_shadow_outbox_schema = {};
|
|
2855
|
+
__export(exports_shadow_outbox_schema, {
|
|
2856
|
+
installShadowOutboxSchema: () => installShadowOutboxSchema,
|
|
2857
|
+
SHADOW_TRIGGER_TABLES: () => SHADOW_TRIGGER_TABLES
|
|
2858
|
+
});
|
|
2859
|
+
function installShadowOutboxSchema(db) {
|
|
2860
|
+
db.run(`CREATE TABLE IF NOT EXISTS shadow_outbox (
|
|
2861
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2862
|
+
object_type TEXT NOT NULL,
|
|
2863
|
+
object_id TEXT NOT NULL,
|
|
2864
|
+
op TEXT NOT NULL,
|
|
2865
|
+
enqueued_at INTEGER NOT NULL,
|
|
2866
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
2867
|
+
next_attempt_at INTEGER NOT NULL DEFAULT 0,
|
|
2868
|
+
last_error TEXT,
|
|
2869
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
2870
|
+
revision INTEGER NOT NULL DEFAULT 0
|
|
2871
|
+
)`);
|
|
2872
|
+
ensureColumn(db, "shadow_outbox", "revision", "INTEGER NOT NULL DEFAULT 0");
|
|
2873
|
+
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS shadow_outbox_pending_uq
|
|
2874
|
+
ON shadow_outbox(object_type, object_id) WHERE status='pending'`);
|
|
2875
|
+
db.run(`CREATE INDEX IF NOT EXISTS shadow_outbox_ready_idx
|
|
2876
|
+
ON shadow_outbox(status, next_attempt_at)`);
|
|
2877
|
+
for (const { table, objectType, deletes } of SHADOW_TRIGGER_TABLES) {
|
|
2878
|
+
const exists = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(table);
|
|
2879
|
+
if (!exists)
|
|
2880
|
+
continue;
|
|
2881
|
+
const insertTrigger = `shadow_ob_${table}_ai`;
|
|
2882
|
+
const updateTrigger = `shadow_ob_${table}_au`;
|
|
2883
|
+
db.run(`DROP TRIGGER IF EXISTS ${insertTrigger}`);
|
|
2884
|
+
db.run(`DROP TRIGGER IF EXISTS ${updateTrigger}`);
|
|
2885
|
+
db.run(upsertTriggerSql(insertTrigger, table, objectType, "INSERT", "NEW"));
|
|
2886
|
+
db.run(upsertTriggerSql(updateTrigger, table, objectType, "UPDATE", "NEW"));
|
|
2887
|
+
if (deletes) {
|
|
2888
|
+
const deleteTrigger = `shadow_ob_${table}_ad`;
|
|
2889
|
+
db.run(`DROP TRIGGER IF EXISTS ${deleteTrigger}`);
|
|
2890
|
+
db.run(deleteTriggerSql(deleteTrigger, table, objectType));
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function ensureColumn(db, table, column, definition) {
|
|
2895
|
+
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
2896
|
+
if (rows.some((row) => row.name === column))
|
|
2897
|
+
return;
|
|
2898
|
+
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
2899
|
+
}
|
|
2900
|
+
function upsertTriggerSql(name, table, objectType, event, row) {
|
|
2901
|
+
return `CREATE TRIGGER IF NOT EXISTS ${name}
|
|
2902
|
+
AFTER ${event} ON ${table}
|
|
2903
|
+
BEGIN
|
|
2904
|
+
INSERT INTO shadow_outbox(object_type, object_id, op, enqueued_at, attempts, next_attempt_at, last_error, status, revision)
|
|
2905
|
+
VALUES('${objectType}', ${row}.id, 'upsert', CAST(strftime('%s','now') AS INTEGER) * 1000, 0, 0, NULL, 'pending', 0)
|
|
2906
|
+
ON CONFLICT(object_type, object_id) WHERE status='pending'
|
|
2907
|
+
DO UPDATE SET op='upsert', enqueued_at=excluded.enqueued_at, attempts=0, next_attempt_at=0, last_error=NULL, revision=shadow_outbox.revision + 1;
|
|
2908
|
+
END`;
|
|
2909
|
+
}
|
|
2910
|
+
function deleteTriggerSql(name, table, objectType) {
|
|
2911
|
+
return `CREATE TRIGGER IF NOT EXISTS ${name}
|
|
2912
|
+
AFTER DELETE ON ${table}
|
|
2913
|
+
BEGIN
|
|
2914
|
+
INSERT INTO shadow_outbox(object_type, object_id, op, enqueued_at, attempts, next_attempt_at, last_error, status, revision)
|
|
2915
|
+
VALUES('${objectType}', OLD.id, 'delete', CAST(strftime('%s','now') AS INTEGER) * 1000, 0, 0, NULL, 'pending', 0)
|
|
2916
|
+
ON CONFLICT(object_type, object_id) WHERE status='pending'
|
|
2917
|
+
DO UPDATE SET op='delete', enqueued_at=excluded.enqueued_at, attempts=0, next_attempt_at=0, last_error=NULL, revision=shadow_outbox.revision + 1;
|
|
2918
|
+
END`;
|
|
2919
|
+
}
|
|
2920
|
+
var SHADOW_TRIGGER_TABLES;
|
|
2921
|
+
var init_shadow_outbox_schema = __esm(() => {
|
|
2922
|
+
SHADOW_TRIGGER_TABLES = [
|
|
2923
|
+
{ table: "tasks", objectType: "tasks", deletes: true },
|
|
2924
|
+
{ table: "projects", objectType: "projects", deletes: true },
|
|
2925
|
+
{ table: "project_machine_paths", objectType: "project_machine_paths", deletes: true },
|
|
2926
|
+
{ table: "plans", objectType: "plans", deletes: true },
|
|
2927
|
+
{ table: "agents", objectType: "agents", deletes: false },
|
|
2928
|
+
{ table: "task_lists", objectType: "task_lists", deletes: true },
|
|
2929
|
+
{ table: "task_templates", objectType: "templates", deletes: true },
|
|
2930
|
+
{ table: "task_history", objectType: "audit_history", deletes: false }
|
|
2931
|
+
];
|
|
2932
|
+
});
|
|
2933
|
+
|
|
2646
2934
|
// src/db/database.ts
|
|
2647
2935
|
var exports_database = {};
|
|
2648
2936
|
__export(exports_database, {
|
|
@@ -2733,8 +3021,20 @@ function openDatabase(path) {
|
|
|
2733
3021
|
runMigrations(db);
|
|
2734
3022
|
backfillTaskTags(db);
|
|
2735
3023
|
backfillMachineId(db);
|
|
3024
|
+
maybeInstallShadowCapture(db);
|
|
2736
3025
|
return db;
|
|
2737
3026
|
}
|
|
3027
|
+
function maybeInstallShadowCapture(db) {
|
|
3028
|
+
try {
|
|
3029
|
+
const { isTodosShadowEnabled: isTodosShadowEnabled2 } = (init_config(), __toCommonJS(exports_config));
|
|
3030
|
+
if (!isTodosShadowEnabled2())
|
|
3031
|
+
return;
|
|
3032
|
+
const { installShadowOutboxSchema: installShadowOutboxSchema2 } = (init_shadow_outbox_schema(), __toCommonJS(exports_shadow_outbox_schema));
|
|
3033
|
+
installShadowOutboxSchema2(db);
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
2738
3038
|
function getDatabase(dbPath) {
|
|
2739
3039
|
const path = dbPath || getDbPath();
|
|
2740
3040
|
if (_db && _dbPath === path)
|
|
@@ -3111,7 +3411,7 @@ function getCompletionGuardConfig(projectPath) {
|
|
|
3111
3411
|
return global;
|
|
3112
3412
|
}
|
|
3113
3413
|
var cached = null, GUARD_DEFAULTS;
|
|
3114
|
-
var
|
|
3414
|
+
var init_config2 = __esm(() => {
|
|
3115
3415
|
init_sync_utils();
|
|
3116
3416
|
GUARD_DEFAULTS = {
|
|
3117
3417
|
enabled: false,
|
|
@@ -3482,7 +3782,7 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
3482
3782
|
}
|
|
3483
3783
|
var init_completion_guard = __esm(() => {
|
|
3484
3784
|
init_types();
|
|
3485
|
-
|
|
3785
|
+
init_config2();
|
|
3486
3786
|
init_projects();
|
|
3487
3787
|
});
|
|
3488
3788
|
|
|
@@ -3490,8 +3790,8 @@ var init_completion_guard = __esm(() => {
|
|
|
3490
3790
|
import { tmpdir } from "os";
|
|
3491
3791
|
import { resolve as resolve3, sep } from "path";
|
|
3492
3792
|
function envFlag(name) {
|
|
3493
|
-
const value = process.env[name];
|
|
3494
|
-
return value === "1" || value === "true" || value === "yes";
|
|
3793
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
3794
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
3495
3795
|
}
|
|
3496
3796
|
function isUnder(parent, child) {
|
|
3497
3797
|
const normalizedParent = resolve3(parent);
|
|
@@ -3517,7 +3817,7 @@ function usesIsolatedTodosHome() {
|
|
|
3517
3817
|
function shouldEmitSharedTaskEvents(dbPath) {
|
|
3518
3818
|
if (envFlag("TODOS_DISABLE_SHARED_EVENTS"))
|
|
3519
3819
|
return false;
|
|
3520
|
-
if (envFlag("TODOS_ALLOW_EPHEMERAL_SHARED_EVENTS"))
|
|
3820
|
+
if (envFlag("TODOS_ALLOW_EPHEMERAL_SHARED_EVENTS") || envFlag("HASNA_TODOS_ALLOW_GLOBAL_EVENTS_FROM_TEMP_DB"))
|
|
3521
3821
|
return true;
|
|
3522
3822
|
if (!isEphemeralTodosDatabase(dbPath))
|
|
3523
3823
|
return true;
|
|
@@ -3619,7 +3919,7 @@ function upsertSecretSafetyConfig(input) {
|
|
|
3619
3919
|
}
|
|
3620
3920
|
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
|
|
3621
3921
|
var init_redaction = __esm(() => {
|
|
3622
|
-
|
|
3922
|
+
init_config2();
|
|
3623
3923
|
DEFAULT_SECRET_PATTERNS = [
|
|
3624
3924
|
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
3625
3925
|
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
@@ -3776,7 +4076,7 @@ function checkWorkspacePermission(input = {}) {
|
|
|
3776
4076
|
}
|
|
3777
4077
|
var DEFAULT_DENYLIST, DEFAULT_ENV_REDACTIONS, PRESET_DEFAULTS;
|
|
3778
4078
|
var init_workspace_trust = __esm(() => {
|
|
3779
|
-
|
|
4079
|
+
init_config2();
|
|
3780
4080
|
DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
3781
4081
|
DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
3782
4082
|
PRESET_DEFAULTS = {
|
|
@@ -4010,7 +4310,7 @@ function explainRunnerSandbox(input = {}) {
|
|
|
4010
4310
|
}
|
|
4011
4311
|
var DEFAULT_COMMAND_DENYLIST, DEFAULT_ENV_REDACTIONS2;
|
|
4012
4312
|
var init_runner_sandbox = __esm(() => {
|
|
4013
|
-
|
|
4313
|
+
init_config2();
|
|
4014
4314
|
init_workspace_trust();
|
|
4015
4315
|
DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
4016
4316
|
DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
@@ -4246,7 +4546,7 @@ var LOCAL_EVENT_TYPES, VALID_TARGETS;
|
|
|
4246
4546
|
var init_event_hooks = __esm(() => {
|
|
4247
4547
|
init_redaction();
|
|
4248
4548
|
init_runner_sandbox();
|
|
4249
|
-
|
|
4549
|
+
init_config2();
|
|
4250
4550
|
init_event_emission_safety();
|
|
4251
4551
|
LOCAL_EVENT_TYPES = [
|
|
4252
4552
|
"task.created",
|
|
@@ -16636,7 +16936,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
|
|
|
16636
16936
|
init_audit();
|
|
16637
16937
|
init_database();
|
|
16638
16938
|
init_tasks();
|
|
16639
|
-
|
|
16939
|
+
init_config2();
|
|
16640
16940
|
init_event_emission_safety();
|
|
16641
16941
|
init_event_hooks();
|
|
16642
16942
|
|
|
@@ -17260,7 +17560,7 @@ init_task_lifecycle();
|
|
|
17260
17560
|
init_tasks();
|
|
17261
17561
|
init_plans();
|
|
17262
17562
|
init_task_runs();
|
|
17263
|
-
|
|
17563
|
+
init_config2();
|
|
17264
17564
|
var LOCAL_ROADMAP_SCHEMA_VERSION = 1;
|
|
17265
17565
|
function timestamp() {
|
|
17266
17566
|
return new Date().toISOString();
|
|
@@ -17626,7 +17926,7 @@ function renderRoadmapMarkdown(idOrName, db) {
|
|
|
17626
17926
|
// src/lib/audit-ledger.ts
|
|
17627
17927
|
init_database();
|
|
17628
17928
|
init_task_runs();
|
|
17629
|
-
|
|
17929
|
+
init_config2();
|
|
17630
17930
|
init_redaction();
|
|
17631
17931
|
import { createHash as createHash5 } from "crypto";
|
|
17632
17932
|
var LOCAL_AUDIT_LEDGER_SCHEMA_VERSION = 1;
|
|
@@ -19843,7 +20143,7 @@ init_task_runs();
|
|
|
19843
20143
|
init_event_hooks();
|
|
19844
20144
|
|
|
19845
20145
|
// src/lib/terminal-notifications.ts
|
|
19846
|
-
|
|
20146
|
+
init_config2();
|
|
19847
20147
|
init_event_hooks();
|
|
19848
20148
|
init_redaction();
|
|
19849
20149
|
var SEVERITY_ORDER = {
|
|
@@ -21013,7 +21313,7 @@ function renderLocalReportMarkdown(report) {
|
|
|
21013
21313
|
`;
|
|
21014
21314
|
}
|
|
21015
21315
|
// src/lib/local-encryption.ts
|
|
21016
|
-
|
|
21316
|
+
init_config2();
|
|
21017
21317
|
init_redaction();
|
|
21018
21318
|
import { createCipheriv, createDecipheriv, createHash as createHash8, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
21019
21319
|
var TODOS_ENCRYPTED_VALUE_KIND = "hasna.todos.encrypted-value";
|
|
@@ -21842,168 +22142,9 @@ var TODOS_REGISTRY = createTodosRegistry({
|
|
|
21842
22142
|
generatedAt: "1970-01-01T00:00:00.000Z"
|
|
21843
22143
|
});
|
|
21844
22144
|
|
|
21845
|
-
// src/storage/
|
|
21846
|
-
|
|
21847
|
-
|
|
21848
|
-
"todos_sync_cursors"
|
|
21849
|
-
];
|
|
21850
|
-
var STORAGE_TABLES = TODOS_STORAGE_TABLES;
|
|
21851
|
-
var TODOS_STORAGE_ENV = {
|
|
21852
|
-
mode: "HASNA_TODOS_STORAGE_MODE",
|
|
21853
|
-
databaseUrl: "HASNA_TODOS_DATABASE_URL",
|
|
21854
|
-
databaseSsl: "HASNA_TODOS_DATABASE_SSL",
|
|
21855
|
-
databaseSchema: "HASNA_TODOS_DATABASE_SCHEMA",
|
|
21856
|
-
s3Bucket: "HASNA_TODOS_S3_BUCKET",
|
|
21857
|
-
s3Prefix: "HASNA_TODOS_S3_PREFIX",
|
|
21858
|
-
awsRegion: "HASNA_TODOS_AWS_REGION",
|
|
21859
|
-
s3Endpoint: "HASNA_TODOS_S3_ENDPOINT",
|
|
21860
|
-
s3ForcePathStyle: "HASNA_TODOS_S3_FORCE_PATH_STYLE",
|
|
21861
|
-
s3AccessKeyId: "HASNA_TODOS_S3_ACCESS_KEY_ID",
|
|
21862
|
-
s3SecretAccessKey: "HASNA_TODOS_S3_SECRET_ACCESS_KEY",
|
|
21863
|
-
s3SessionToken: "HASNA_TODOS_S3_SESSION_TOKEN",
|
|
21864
|
-
syncBatchSize: "HASNA_TODOS_SYNC_BATCH_SIZE",
|
|
21865
|
-
syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
|
|
21866
|
-
};
|
|
21867
|
-
var TODOS_STORAGE_FALLBACK_ENV = {
|
|
21868
|
-
mode: "TODOS_STORAGE_MODE",
|
|
21869
|
-
databaseUrl: "TODOS_DATABASE_URL",
|
|
21870
|
-
databaseSsl: "TODOS_DATABASE_SSL",
|
|
21871
|
-
databaseSchema: "TODOS_DATABASE_SCHEMA",
|
|
21872
|
-
s3Bucket: "TODOS_S3_BUCKET",
|
|
21873
|
-
s3Prefix: "TODOS_S3_PREFIX",
|
|
21874
|
-
awsRegion: "TODOS_AWS_REGION",
|
|
21875
|
-
s3Endpoint: "TODOS_S3_ENDPOINT",
|
|
21876
|
-
s3ForcePathStyle: "TODOS_S3_FORCE_PATH_STYLE",
|
|
21877
|
-
s3AccessKeyId: "TODOS_S3_ACCESS_KEY_ID",
|
|
21878
|
-
s3SecretAccessKey: "TODOS_S3_SECRET_ACCESS_KEY",
|
|
21879
|
-
s3SessionToken: "TODOS_S3_SESSION_TOKEN",
|
|
21880
|
-
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
21881
|
-
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
21882
|
-
};
|
|
21883
|
-
var CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
|
|
21884
|
-
var CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
21885
|
-
var CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
|
|
21886
|
-
function getCanonicalTodosRdsConfig() {
|
|
21887
|
-
return {
|
|
21888
|
-
cluster: CANONICAL_TODOS_RDS_CLUSTER,
|
|
21889
|
-
database: CANONICAL_TODOS_RDS_DATABASE,
|
|
21890
|
-
runtimeSecretPath: CANONICAL_TODOS_RDS_RUNTIME_PATH,
|
|
21891
|
-
primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
|
|
21892
|
-
fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
|
|
21893
|
-
};
|
|
21894
|
-
}
|
|
21895
|
-
function loadTodosStorageConfig(env = process.env) {
|
|
21896
|
-
const mode = getTodosStorageMode(env);
|
|
21897
|
-
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
21898
|
-
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
21899
|
-
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
21900
|
-
const region = readStorageEnv(env, "awsRegion").value;
|
|
21901
|
-
const endpoint = readStorageEnv(env, "s3Endpoint").value;
|
|
21902
|
-
const schema = readStorageEnv(env, "databaseSchema").value;
|
|
21903
|
-
return {
|
|
21904
|
-
service: "todos",
|
|
21905
|
-
mode,
|
|
21906
|
-
...databaseUrl ? {
|
|
21907
|
-
database: {
|
|
21908
|
-
provider: "postgres",
|
|
21909
|
-
url: databaseUrl,
|
|
21910
|
-
ssl: parseBoolean(readStorageEnv(env, "databaseSsl").value, true),
|
|
21911
|
-
...schema ? { schema } : {}
|
|
21912
|
-
}
|
|
21913
|
-
} : {},
|
|
21914
|
-
...bucket ? {
|
|
21915
|
-
objectStorage: {
|
|
21916
|
-
provider: "s3",
|
|
21917
|
-
bucket,
|
|
21918
|
-
prefix,
|
|
21919
|
-
...region ? { region } : {},
|
|
21920
|
-
...endpoint ? { endpoint } : {},
|
|
21921
|
-
forcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value, false)
|
|
21922
|
-
}
|
|
21923
|
-
} : {},
|
|
21924
|
-
sync: {
|
|
21925
|
-
batchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value, 500),
|
|
21926
|
-
dryRun: parseBoolean(readStorageEnv(env, "syncDryRun").value, false)
|
|
21927
|
-
}
|
|
21928
|
-
};
|
|
21929
|
-
}
|
|
21930
|
-
function loadStorageConfig(env = process.env) {
|
|
21931
|
-
return loadTodosStorageConfig(env);
|
|
21932
|
-
}
|
|
21933
|
-
function isTodosRemoteStorageEnabled(config) {
|
|
21934
|
-
return config.mode === "remote" || config.mode === "hybrid";
|
|
21935
|
-
}
|
|
21936
|
-
function assertTodosRemoteStorageConfig(config) {
|
|
21937
|
-
if (!isTodosRemoteStorageEnabled(config))
|
|
21938
|
-
return;
|
|
21939
|
-
if (!config.database?.url) {
|
|
21940
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
21941
|
-
}
|
|
21942
|
-
}
|
|
21943
|
-
function parseStorageMode(value) {
|
|
21944
|
-
const normalized = clean(value)?.toLowerCase();
|
|
21945
|
-
if (!normalized)
|
|
21946
|
-
return "local";
|
|
21947
|
-
if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
|
|
21948
|
-
return normalized;
|
|
21949
|
-
throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
|
|
21950
|
-
}
|
|
21951
|
-
function getTodosStorageMode(env = process.env) {
|
|
21952
|
-
return parseStorageMode(readStorageEnv(env, "mode").value);
|
|
21953
|
-
}
|
|
21954
|
-
function getStorageMode(env = process.env) {
|
|
21955
|
-
return getTodosStorageMode(env);
|
|
21956
|
-
}
|
|
21957
|
-
function getTodosStorageDatabaseEnv(env = process.env) {
|
|
21958
|
-
return readStorageEnv(env, "databaseUrl").name;
|
|
21959
|
-
}
|
|
21960
|
-
function getStorageDatabaseEnv(env = process.env) {
|
|
21961
|
-
return getTodosStorageDatabaseEnv(env);
|
|
21962
|
-
}
|
|
21963
|
-
function getTodosStorageDatabaseUrl(env = process.env) {
|
|
21964
|
-
return readStorageEnv(env, "databaseUrl").value;
|
|
21965
|
-
}
|
|
21966
|
-
function getStorageDatabaseUrl(env = process.env) {
|
|
21967
|
-
return getTodosStorageDatabaseUrl(env);
|
|
21968
|
-
}
|
|
21969
|
-
function getTodosStorageEnvName(env, key) {
|
|
21970
|
-
return readStorageEnv(env, key).name;
|
|
21971
|
-
}
|
|
21972
|
-
function readStorageEnv(env, key) {
|
|
21973
|
-
const primaryName = TODOS_STORAGE_ENV[key];
|
|
21974
|
-
const primaryValue = clean(env[primaryName]);
|
|
21975
|
-
if (primaryValue !== undefined)
|
|
21976
|
-
return { name: primaryName, value: primaryValue };
|
|
21977
|
-
const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
|
|
21978
|
-
const fallbackValue = clean(env[fallbackName]);
|
|
21979
|
-
if (fallbackValue !== undefined)
|
|
21980
|
-
return { name: fallbackName, value: fallbackValue };
|
|
21981
|
-
return { name: primaryName };
|
|
21982
|
-
}
|
|
21983
|
-
function clean(value) {
|
|
21984
|
-
const trimmed = value?.trim();
|
|
21985
|
-
return trimmed ? trimmed : undefined;
|
|
21986
|
-
}
|
|
21987
|
-
function parseBoolean(value, fallback) {
|
|
21988
|
-
const normalized = clean(value)?.toLowerCase();
|
|
21989
|
-
if (!normalized)
|
|
21990
|
-
return fallback;
|
|
21991
|
-
if (["1", "true", "yes", "on"].includes(normalized))
|
|
21992
|
-
return true;
|
|
21993
|
-
if (["0", "false", "no", "off"].includes(normalized))
|
|
21994
|
-
return false;
|
|
21995
|
-
throw new Error(`Expected boolean env value, got ${value}`);
|
|
21996
|
-
}
|
|
21997
|
-
function parsePositiveInteger(value, fallback) {
|
|
21998
|
-
const normalized = clean(value);
|
|
21999
|
-
if (!normalized)
|
|
22000
|
-
return fallback;
|
|
22001
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
22002
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
22003
|
-
throw new Error(`${TODOS_STORAGE_ENV.syncBatchSize} must be a positive integer`);
|
|
22004
|
-
}
|
|
22005
|
-
return parsed;
|
|
22006
|
-
}
|
|
22145
|
+
// src/storage/index.ts
|
|
22146
|
+
init_config();
|
|
22147
|
+
|
|
22007
22148
|
// src/storage/local-sqlite.ts
|
|
22008
22149
|
init_tasks();
|
|
22009
22150
|
init_projects();
|
|
@@ -23079,6 +23220,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
23079
23220
|
audit: {
|
|
23080
23221
|
logTaskChange: (taskId, action, field2, oldValue, newValue, agentId) => logTaskChange(taskId, action, field2, oldValue, newValue, agentId, database()),
|
|
23081
23222
|
addComment: (input) => addComment(input, database()),
|
|
23223
|
+
getComments: (taskId) => listComments(taskId, database()),
|
|
23082
23224
|
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
23083
23225
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
23084
23226
|
},
|
|
@@ -23114,6 +23256,9 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
|
|
|
23114
23256
|
PRIMARY KEY (service, object_type, object_id)
|
|
23115
23257
|
)`,
|
|
23116
23258
|
`CREATE INDEX IF NOT EXISTS ${tableName}_updated_idx ON ${tableName} (service, updated_at)`,
|
|
23259
|
+
`CREATE INDEX IF NOT EXISTS ${tableName}_task_status_idx ON ${tableName} ((payload->>'status')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
23260
|
+
`CREATE INDEX IF NOT EXISTS ${tableName}_task_project_idx ON ${tableName} ((payload->>'project_id')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
23261
|
+
`CREATE INDEX IF NOT EXISTS ${tableName}_payload_gin ON ${tableName} USING gin (payload jsonb_path_ops)`,
|
|
23117
23262
|
`CREATE TABLE IF NOT EXISTS ${cursorTableName} (
|
|
23118
23263
|
service text NOT NULL,
|
|
23119
23264
|
cursor_name text NOT NULL,
|
|
@@ -23164,7 +23309,7 @@ class PostgresTodosSyncStore {
|
|
|
23164
23309
|
this.service,
|
|
23165
23310
|
entry.type,
|
|
23166
23311
|
entry.id,
|
|
23167
|
-
|
|
23312
|
+
entry.payload,
|
|
23168
23313
|
entry.updatedAt,
|
|
23169
23314
|
entry.deletedAt,
|
|
23170
23315
|
sourceMachineId,
|
|
@@ -23370,8 +23515,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
23370
23515
|
tasks: {
|
|
23371
23516
|
create: (input, context) => createTask2(input, store, context),
|
|
23372
23517
|
get: (id) => store.get("tasks", id),
|
|
23373
|
-
list: (filter = {}) =>
|
|
23374
|
-
count:
|
|
23518
|
+
list: (filter = {}) => store.listTasks(filter),
|
|
23519
|
+
count: (filter = {}) => store.countTasks(filter),
|
|
23375
23520
|
update: (id, input) => updateTask2(id, input, store),
|
|
23376
23521
|
delete: (id, context) => store.delete("tasks", id, context),
|
|
23377
23522
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
@@ -23426,6 +23571,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
23426
23571
|
audit: {
|
|
23427
23572
|
logTaskChange: (taskId, action, field2, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field2, oldValue, newValue, agentId, store, context),
|
|
23428
23573
|
addComment: (input, context) => addComment2(input, store, context),
|
|
23574
|
+
getComments: async (taskId) => (await store.list("comments")).filter((comment) => comment.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
|
|
23429
23575
|
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
|
|
23430
23576
|
getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
|
|
23431
23577
|
},
|
|
@@ -23487,6 +23633,72 @@ class PostgresJsonRecordStore {
|
|
|
23487
23633
|
updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
|
|
23488
23634
|
}));
|
|
23489
23635
|
}
|
|
23636
|
+
buildTaskFilterSql(filter) {
|
|
23637
|
+
const params = [this.service, "tasks"];
|
|
23638
|
+
const conds = ["service = $1", "object_type = $2", "deleted_at IS NULL"];
|
|
23639
|
+
const p = (value) => {
|
|
23640
|
+
params.push(value);
|
|
23641
|
+
return `$${params.length}`;
|
|
23642
|
+
};
|
|
23643
|
+
const inClause = (column, values) => {
|
|
23644
|
+
if (values.length === 0)
|
|
23645
|
+
return "1=0";
|
|
23646
|
+
return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
|
|
23647
|
+
};
|
|
23648
|
+
if (filter.ids)
|
|
23649
|
+
conds.push(inClause("payload->>'id'", filter.ids));
|
|
23650
|
+
if (filter.project_id !== undefined)
|
|
23651
|
+
conds.push(`payload->>'project_id' = ${p(filter.project_id)}`);
|
|
23652
|
+
if (filter.parent_id !== undefined)
|
|
23653
|
+
conds.push(`payload->>'parent_id' IS NOT DISTINCT FROM ${p(filter.parent_id)}`);
|
|
23654
|
+
if (filter.plan_id !== undefined)
|
|
23655
|
+
conds.push(`payload->>'plan_id' = ${p(filter.plan_id)}`);
|
|
23656
|
+
if (filter.task_list_id !== undefined)
|
|
23657
|
+
conds.push(`payload->>'task_list_id' = ${p(filter.task_list_id)}`);
|
|
23658
|
+
if (filter.status !== undefined)
|
|
23659
|
+
conds.push(inClause("payload->>'status'", toFilterArray(filter.status)));
|
|
23660
|
+
if (filter.priority !== undefined)
|
|
23661
|
+
conds.push(inClause("payload->>'priority'", toFilterArray(filter.priority)));
|
|
23662
|
+
if (filter.assigned_to !== undefined)
|
|
23663
|
+
conds.push(`payload->>'assigned_to' = ${p(filter.assigned_to)}`);
|
|
23664
|
+
if (filter.agent_id !== undefined)
|
|
23665
|
+
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
23666
|
+
if (filter.session_id !== undefined)
|
|
23667
|
+
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
23668
|
+
if (filter.tags?.length)
|
|
23669
|
+
conds.push(`payload->'tags' @> ${p(filter.tags)}::jsonb`);
|
|
23670
|
+
if (filter.has_recurrence !== undefined) {
|
|
23671
|
+
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
23672
|
+
}
|
|
23673
|
+
if (filter.task_type !== undefined) {
|
|
23674
|
+
conds.push(inClause("COALESCE(payload->>'task_type', '')", toFilterArray(filter.task_type)));
|
|
23675
|
+
}
|
|
23676
|
+
if (filter.include_subtasks !== true)
|
|
23677
|
+
conds.push(`(payload->>'parent_id' IS NULL OR payload->>'parent_id' = '')`);
|
|
23678
|
+
return { where: conds.join(" AND "), params };
|
|
23679
|
+
}
|
|
23680
|
+
async listTasks(filter) {
|
|
23681
|
+
await this.ensureSchema();
|
|
23682
|
+
const { where, params } = this.buildTaskFilterSql(filter);
|
|
23683
|
+
let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${TASK_ORDER_BY}`;
|
|
23684
|
+
if (filter.limit !== undefined) {
|
|
23685
|
+
params.push(filter.limit);
|
|
23686
|
+
sql += ` LIMIT $${params.length}`;
|
|
23687
|
+
}
|
|
23688
|
+
if (filter.offset) {
|
|
23689
|
+
params.push(filter.offset);
|
|
23690
|
+
sql += ` OFFSET $${params.length}`;
|
|
23691
|
+
}
|
|
23692
|
+
const result = await this.options.client.query(sql, params);
|
|
23693
|
+
return result.rows.map((row) => payloadRecord2(row.payload));
|
|
23694
|
+
}
|
|
23695
|
+
async countTasks(filter) {
|
|
23696
|
+
await this.ensureSchema();
|
|
23697
|
+
const { where, params } = this.buildTaskFilterSql(filter);
|
|
23698
|
+
const sql = `/* todos:count-tasks */ SELECT COUNT(*)::int AS count FROM ${this.tableName} WHERE ${where}`;
|
|
23699
|
+
const result = await this.options.client.query(sql, params);
|
|
23700
|
+
return Number(result.rows[0]?.count ?? 0);
|
|
23701
|
+
}
|
|
23490
23702
|
async listTombstones() {
|
|
23491
23703
|
await this.ensureSchema();
|
|
23492
23704
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -23527,7 +23739,7 @@ class PostgresJsonRecordStore {
|
|
|
23527
23739
|
this.service,
|
|
23528
23740
|
type,
|
|
23529
23741
|
value.id,
|
|
23530
|
-
|
|
23742
|
+
jsonbParam(value),
|
|
23531
23743
|
updatedAt,
|
|
23532
23744
|
context.requestId ?? this.sourceMachineId ?? null,
|
|
23533
23745
|
numberValue3(value.version)
|
|
@@ -23589,7 +23801,7 @@ class PostgresJsonRecordStore {
|
|
|
23589
23801
|
this.service,
|
|
23590
23802
|
tombstone.object_type,
|
|
23591
23803
|
tombstone.object_id,
|
|
23592
|
-
|
|
23804
|
+
jsonbParam(tombstone.payload ?? { id: tombstone.object_id, deleted_at: deletedAt }),
|
|
23593
23805
|
updatedAt,
|
|
23594
23806
|
deletedAt,
|
|
23595
23807
|
tombstone.source_machine_id ?? context.requestId ?? this.sourceMachineId ?? null,
|
|
@@ -23768,13 +23980,15 @@ async function patchTask(task2, patch, store) {
|
|
|
23768
23980
|
await store.upsert("tasks", updated);
|
|
23769
23981
|
return updated;
|
|
23770
23982
|
}
|
|
23983
|
+
var TASK_ORDER_BY = "ORDER BY CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC";
|
|
23984
|
+
function toFilterArray(value) {
|
|
23985
|
+
return Array.isArray(value) ? value : [value];
|
|
23986
|
+
}
|
|
23987
|
+
function jsonbParam(value) {
|
|
23988
|
+
return value;
|
|
23989
|
+
}
|
|
23771
23990
|
async function listTasks2(filter, store) {
|
|
23772
|
-
|
|
23773
|
-
tasks = tasks.filter((task2) => taskMatchesFilter(task2, filter));
|
|
23774
|
-
tasks.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority) || a.created_at.localeCompare(b.created_at));
|
|
23775
|
-
const offset = filter.offset ?? 0;
|
|
23776
|
-
const limit = filter.limit ?? tasks.length;
|
|
23777
|
-
return tasks.slice(offset, offset + limit);
|
|
23991
|
+
return store.listTasks(filter);
|
|
23778
23992
|
}
|
|
23779
23993
|
async function getNextTask2(filters, store) {
|
|
23780
23994
|
return (await listTasks2({ ...filters, status: "pending", limit: 1 }, store))[0] ?? null;
|
|
@@ -24082,43 +24296,6 @@ async function generateProjectPrefix(name, store) {
|
|
|
24082
24296
|
}
|
|
24083
24297
|
return candidate;
|
|
24084
24298
|
}
|
|
24085
|
-
function taskMatchesFilter(task2, filter) {
|
|
24086
|
-
if (filter.ids && !filter.ids.includes(task2.id))
|
|
24087
|
-
return false;
|
|
24088
|
-
if (filter.project_id !== undefined && task2.project_id !== filter.project_id)
|
|
24089
|
-
return false;
|
|
24090
|
-
if (filter.parent_id !== undefined && task2.parent_id !== filter.parent_id)
|
|
24091
|
-
return false;
|
|
24092
|
-
if (filter.plan_id !== undefined && task2.plan_id !== filter.plan_id)
|
|
24093
|
-
return false;
|
|
24094
|
-
if (filter.task_list_id !== undefined && task2.task_list_id !== filter.task_list_id)
|
|
24095
|
-
return false;
|
|
24096
|
-
if (filter.status !== undefined && !matchesOne(task2.status, filter.status))
|
|
24097
|
-
return false;
|
|
24098
|
-
if (filter.priority !== undefined && !matchesOne(task2.priority, filter.priority))
|
|
24099
|
-
return false;
|
|
24100
|
-
if (filter.assigned_to !== undefined && task2.assigned_to !== filter.assigned_to)
|
|
24101
|
-
return false;
|
|
24102
|
-
if (filter.agent_id !== undefined && task2.agent_id !== filter.agent_id)
|
|
24103
|
-
return false;
|
|
24104
|
-
if (filter.session_id !== undefined && task2.session_id !== filter.session_id)
|
|
24105
|
-
return false;
|
|
24106
|
-
if (filter.tags?.length && !filter.tags.every((tag) => task2.tags.includes(tag)))
|
|
24107
|
-
return false;
|
|
24108
|
-
if (filter.has_recurrence !== undefined && Boolean(task2.recurrence_rule) !== filter.has_recurrence)
|
|
24109
|
-
return false;
|
|
24110
|
-
if (filter.include_subtasks !== true && task2.parent_id)
|
|
24111
|
-
return false;
|
|
24112
|
-
if (filter.task_type !== undefined && !matchesOne(task2.task_type ?? "", filter.task_type))
|
|
24113
|
-
return false;
|
|
24114
|
-
return true;
|
|
24115
|
-
}
|
|
24116
|
-
function matchesOne(value, expected) {
|
|
24117
|
-
return Array.isArray(expected) ? expected.includes(value) : value === expected;
|
|
24118
|
-
}
|
|
24119
|
-
function priorityRank(priority) {
|
|
24120
|
-
return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
|
|
24121
|
-
}
|
|
24122
24299
|
function slugifyRaw(value) {
|
|
24123
24300
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
24124
24301
|
}
|
|
@@ -24178,11 +24355,424 @@ function numberValue3(value) {
|
|
|
24178
24355
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
24179
24356
|
}
|
|
24180
24357
|
|
|
24358
|
+
// src/storage/shadow.ts
|
|
24359
|
+
var SNAPSHOT_TO_OBJECT_TYPE = {
|
|
24360
|
+
tasks: "tasks",
|
|
24361
|
+
projects: "projects",
|
|
24362
|
+
plans: "plans",
|
|
24363
|
+
agents: "agents",
|
|
24364
|
+
taskLists: "task_lists",
|
|
24365
|
+
templates: "templates",
|
|
24366
|
+
auditHistory: "audit_history"
|
|
24367
|
+
};
|
|
24368
|
+
|
|
24369
|
+
class TodosShadowMirror {
|
|
24370
|
+
queue = [];
|
|
24371
|
+
syncStore;
|
|
24372
|
+
maxRetries;
|
|
24373
|
+
retryBaseMs;
|
|
24374
|
+
onEvent;
|
|
24375
|
+
schemaReady;
|
|
24376
|
+
pumping = false;
|
|
24377
|
+
scheduledRetries = 0;
|
|
24378
|
+
idleResolvers = [];
|
|
24379
|
+
metrics = {
|
|
24380
|
+
enqueued: 0,
|
|
24381
|
+
mirrored: 0,
|
|
24382
|
+
retries: 0,
|
|
24383
|
+
failed: 0,
|
|
24384
|
+
lastMirrorAt: null,
|
|
24385
|
+
lastLagMs: null,
|
|
24386
|
+
lastError: null
|
|
24387
|
+
};
|
|
24388
|
+
constructor(options) {
|
|
24389
|
+
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, {
|
|
24390
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
24391
|
+
}) : null);
|
|
24392
|
+
if (!syncStore) {
|
|
24393
|
+
throw new Error("shadow mirror requires a Postgres sync store or query client");
|
|
24394
|
+
}
|
|
24395
|
+
this.syncStore = syncStore;
|
|
24396
|
+
this.maxRetries = Math.max(0, options.maxRetries ?? 5);
|
|
24397
|
+
this.retryBaseMs = Math.max(1, options.retryBaseMs ?? 250);
|
|
24398
|
+
if (options.onEvent)
|
|
24399
|
+
this.onEvent = options.onEvent;
|
|
24400
|
+
this.schemaReady = options.ensureSchema === false ? Promise.resolve() : null;
|
|
24401
|
+
}
|
|
24402
|
+
getMetrics() {
|
|
24403
|
+
return {
|
|
24404
|
+
enabled: true,
|
|
24405
|
+
enqueued: this.metrics.enqueued,
|
|
24406
|
+
mirrored: this.metrics.mirrored,
|
|
24407
|
+
retries: this.metrics.retries,
|
|
24408
|
+
failed: this.metrics.failed,
|
|
24409
|
+
divergence: this.metrics.failed + this.queue.length + this.scheduledRetries,
|
|
24410
|
+
pending: this.queue.length + this.scheduledRetries,
|
|
24411
|
+
inFlight: this.pumping || this.scheduledRetries > 0,
|
|
24412
|
+
lastMirrorAt: this.metrics.lastMirrorAt,
|
|
24413
|
+
lastLagMs: this.metrics.lastLagMs,
|
|
24414
|
+
lastError: this.metrics.lastError
|
|
24415
|
+
};
|
|
24416
|
+
}
|
|
24417
|
+
enqueueUpsert(key, record, context) {
|
|
24418
|
+
if (!record || typeof record !== "object")
|
|
24419
|
+
return;
|
|
24420
|
+
const payload = record;
|
|
24421
|
+
if (typeof payload["id"] !== "string" || !payload["id"])
|
|
24422
|
+
return;
|
|
24423
|
+
this.metrics.enqueued += 1;
|
|
24424
|
+
this.queue.push({
|
|
24425
|
+
kind: "upsert",
|
|
24426
|
+
key,
|
|
24427
|
+
record: payload,
|
|
24428
|
+
...context ? { context } : {},
|
|
24429
|
+
enqueuedAt: Date.now(),
|
|
24430
|
+
attempts: 0
|
|
24431
|
+
});
|
|
24432
|
+
this.pump();
|
|
24433
|
+
}
|
|
24434
|
+
enqueueDelete(objectType, id, context) {
|
|
24435
|
+
if (!id)
|
|
24436
|
+
return;
|
|
24437
|
+
this.metrics.enqueued += 1;
|
|
24438
|
+
this.queue.push({
|
|
24439
|
+
kind: "delete",
|
|
24440
|
+
objectType,
|
|
24441
|
+
id,
|
|
24442
|
+
...context ? { context } : {},
|
|
24443
|
+
enqueuedAt: Date.now(),
|
|
24444
|
+
attempts: 0
|
|
24445
|
+
});
|
|
24446
|
+
this.pump();
|
|
24447
|
+
}
|
|
24448
|
+
async flush() {
|
|
24449
|
+
if (this.idle())
|
|
24450
|
+
return;
|
|
24451
|
+
await new Promise((resolve10) => {
|
|
24452
|
+
this.idleResolvers.push(resolve10);
|
|
24453
|
+
this.pump();
|
|
24454
|
+
});
|
|
24455
|
+
}
|
|
24456
|
+
idle() {
|
|
24457
|
+
return this.queue.length === 0 && !this.pumping && this.scheduledRetries === 0;
|
|
24458
|
+
}
|
|
24459
|
+
notifyIdle() {
|
|
24460
|
+
if (!this.idle())
|
|
24461
|
+
return;
|
|
24462
|
+
const resolvers = this.idleResolvers;
|
|
24463
|
+
this.idleResolvers = [];
|
|
24464
|
+
for (const resolve10 of resolvers)
|
|
24465
|
+
resolve10();
|
|
24466
|
+
}
|
|
24467
|
+
pump() {
|
|
24468
|
+
if (this.pumping)
|
|
24469
|
+
return;
|
|
24470
|
+
this.pumping = true;
|
|
24471
|
+
this.drain();
|
|
24472
|
+
}
|
|
24473
|
+
async drain() {
|
|
24474
|
+
try {
|
|
24475
|
+
while (this.queue.length > 0) {
|
|
24476
|
+
const op = this.queue.shift();
|
|
24477
|
+
await this.process(op);
|
|
24478
|
+
}
|
|
24479
|
+
} finally {
|
|
24480
|
+
this.pumping = false;
|
|
24481
|
+
this.notifyIdle();
|
|
24482
|
+
}
|
|
24483
|
+
}
|
|
24484
|
+
async process(op) {
|
|
24485
|
+
try {
|
|
24486
|
+
await this.ensureSchema();
|
|
24487
|
+
await this.push(op);
|
|
24488
|
+
const objectType = op.kind === "upsert" ? SNAPSHOT_TO_OBJECT_TYPE[op.key] : op.objectType;
|
|
24489
|
+
const id = op.kind === "upsert" ? String(op.record["id"]) : op.id;
|
|
24490
|
+
const lagMs = Date.now() - op.enqueuedAt;
|
|
24491
|
+
this.metrics.mirrored += 1;
|
|
24492
|
+
this.metrics.lastMirrorAt = new Date().toISOString();
|
|
24493
|
+
this.metrics.lastLagMs = lagMs;
|
|
24494
|
+
this.onEvent?.({ type: "mirrored", objectType, id, lagMs });
|
|
24495
|
+
} catch (error) {
|
|
24496
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24497
|
+
const objectType = op.kind === "upsert" ? SNAPSHOT_TO_OBJECT_TYPE[op.key] : op.objectType;
|
|
24498
|
+
const id = op.kind === "upsert" ? String(op.record["id"]) : op.id;
|
|
24499
|
+
this.metrics.retries += 1;
|
|
24500
|
+
this.metrics.lastError = message;
|
|
24501
|
+
op.attempts += 1;
|
|
24502
|
+
if (op.attempts <= this.maxRetries) {
|
|
24503
|
+
this.onEvent?.({ type: "retry", objectType, id, attempt: op.attempts, error: message });
|
|
24504
|
+
const delay = this.retryBaseMs * 2 ** (op.attempts - 1);
|
|
24505
|
+
this.scheduledRetries += 1;
|
|
24506
|
+
setTimeout(() => {
|
|
24507
|
+
this.scheduledRetries -= 1;
|
|
24508
|
+
this.queue.push(op);
|
|
24509
|
+
this.pump();
|
|
24510
|
+
}, delay);
|
|
24511
|
+
return;
|
|
24512
|
+
}
|
|
24513
|
+
this.metrics.failed += 1;
|
|
24514
|
+
this.onEvent?.({ type: "dropped", objectType, id, error: message });
|
|
24515
|
+
}
|
|
24516
|
+
}
|
|
24517
|
+
ensureSchema() {
|
|
24518
|
+
if (!this.schemaReady) {
|
|
24519
|
+
this.schemaReady = this.syncStore.ensureSchema().catch((error) => {
|
|
24520
|
+
this.schemaReady = null;
|
|
24521
|
+
throw error;
|
|
24522
|
+
});
|
|
24523
|
+
}
|
|
24524
|
+
return this.schemaReady;
|
|
24525
|
+
}
|
|
24526
|
+
async push(op) {
|
|
24527
|
+
const snapshot = emptySnapshot();
|
|
24528
|
+
const context = op.context ?? {};
|
|
24529
|
+
if (op.kind === "upsert") {
|
|
24530
|
+
snapshot[op.key].push(op.record);
|
|
24531
|
+
} else {
|
|
24532
|
+
const now4 = new Date().toISOString();
|
|
24533
|
+
const tombstone = {
|
|
24534
|
+
object_type: op.objectType,
|
|
24535
|
+
object_id: op.id,
|
|
24536
|
+
deleted_at: now4,
|
|
24537
|
+
updated_at: now4,
|
|
24538
|
+
payload: { id: op.id, deleted_at: now4 },
|
|
24539
|
+
version: null
|
|
24540
|
+
};
|
|
24541
|
+
snapshot.tombstones = [tombstone];
|
|
24542
|
+
}
|
|
24543
|
+
await this.syncStore.pushSnapshot(snapshot, context);
|
|
24544
|
+
}
|
|
24545
|
+
}
|
|
24546
|
+
function emptySnapshot() {
|
|
24547
|
+
return {
|
|
24548
|
+
exportedAt: new Date().toISOString(),
|
|
24549
|
+
source: "sqlite",
|
|
24550
|
+
tasks: [],
|
|
24551
|
+
projects: [],
|
|
24552
|
+
projectMachinePaths: [],
|
|
24553
|
+
plans: [],
|
|
24554
|
+
agents: [],
|
|
24555
|
+
taskLists: [],
|
|
24556
|
+
templates: [],
|
|
24557
|
+
auditHistory: [],
|
|
24558
|
+
tombstones: []
|
|
24559
|
+
};
|
|
24560
|
+
}
|
|
24561
|
+
function createShadowTodosStorageAdapter(options) {
|
|
24562
|
+
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
24563
|
+
const mirror = new TodosShadowMirror(options);
|
|
24564
|
+
const adapter = {
|
|
24565
|
+
...local,
|
|
24566
|
+
kind: "sqlite",
|
|
24567
|
+
capabilities: { ...local.capabilities, remotePersistence: false, sync: true },
|
|
24568
|
+
tasks: {
|
|
24569
|
+
...local.tasks,
|
|
24570
|
+
async create(input, context) {
|
|
24571
|
+
const task2 = await local.tasks.create(input, context);
|
|
24572
|
+
mirror.enqueueUpsert("tasks", task2, context);
|
|
24573
|
+
return task2;
|
|
24574
|
+
},
|
|
24575
|
+
async update(id, input, context) {
|
|
24576
|
+
const task2 = await local.tasks.update(id, input, context);
|
|
24577
|
+
mirror.enqueueUpsert("tasks", task2, context);
|
|
24578
|
+
return task2;
|
|
24579
|
+
},
|
|
24580
|
+
async start(id, agentId, context) {
|
|
24581
|
+
const task2 = await local.tasks.start(id, agentId, context);
|
|
24582
|
+
mirror.enqueueUpsert("tasks", task2, context);
|
|
24583
|
+
return task2;
|
|
24584
|
+
},
|
|
24585
|
+
async complete(id, agentId, opts, context) {
|
|
24586
|
+
const task2 = await local.tasks.complete(id, agentId, opts, context);
|
|
24587
|
+
mirror.enqueueUpsert("tasks", task2, context);
|
|
24588
|
+
return task2;
|
|
24589
|
+
},
|
|
24590
|
+
async fail(id, agentId, reason, opts, context) {
|
|
24591
|
+
const result = await local.tasks.fail(id, agentId, reason, opts, context);
|
|
24592
|
+
mirror.enqueueUpsert("tasks", result.task, context);
|
|
24593
|
+
if (result.retryTask)
|
|
24594
|
+
mirror.enqueueUpsert("tasks", result.retryTask, context);
|
|
24595
|
+
return result;
|
|
24596
|
+
},
|
|
24597
|
+
async claimNext(agentId, filters, context) {
|
|
24598
|
+
const task2 = await local.tasks.claimNext(agentId, filters, context);
|
|
24599
|
+
if (task2)
|
|
24600
|
+
mirror.enqueueUpsert("tasks", task2, context);
|
|
24601
|
+
return task2;
|
|
24602
|
+
},
|
|
24603
|
+
async delete(id, context) {
|
|
24604
|
+
const deleted = await local.tasks.delete(id, context);
|
|
24605
|
+
if (deleted)
|
|
24606
|
+
mirror.enqueueDelete("tasks", id, context);
|
|
24607
|
+
return deleted;
|
|
24608
|
+
}
|
|
24609
|
+
},
|
|
24610
|
+
projects: {
|
|
24611
|
+
...local.projects,
|
|
24612
|
+
async create(input, context) {
|
|
24613
|
+
const project = await local.projects.create(input, context);
|
|
24614
|
+
mirror.enqueueUpsert("projects", project, context);
|
|
24615
|
+
return project;
|
|
24616
|
+
},
|
|
24617
|
+
async update(id, input, context) {
|
|
24618
|
+
const project = await local.projects.update(id, input, context);
|
|
24619
|
+
mirror.enqueueUpsert("projects", project, context);
|
|
24620
|
+
return project;
|
|
24621
|
+
},
|
|
24622
|
+
async delete(id, context) {
|
|
24623
|
+
const deleted = await local.projects.delete(id, context);
|
|
24624
|
+
if (deleted)
|
|
24625
|
+
mirror.enqueueDelete("projects", id, context);
|
|
24626
|
+
return deleted;
|
|
24627
|
+
}
|
|
24628
|
+
},
|
|
24629
|
+
plans: {
|
|
24630
|
+
...local.plans,
|
|
24631
|
+
async create(input, context) {
|
|
24632
|
+
const plan = await local.plans.create(input, context);
|
|
24633
|
+
mirror.enqueueUpsert("plans", plan, context);
|
|
24634
|
+
return plan;
|
|
24635
|
+
},
|
|
24636
|
+
async update(id, input, context) {
|
|
24637
|
+
const plan = await local.plans.update(id, input, context);
|
|
24638
|
+
mirror.enqueueUpsert("plans", plan, context);
|
|
24639
|
+
return plan;
|
|
24640
|
+
},
|
|
24641
|
+
async delete(id, context) {
|
|
24642
|
+
const deleted = await local.plans.delete(id, context);
|
|
24643
|
+
if (deleted)
|
|
24644
|
+
mirror.enqueueDelete("plans", id, context);
|
|
24645
|
+
return deleted;
|
|
24646
|
+
}
|
|
24647
|
+
},
|
|
24648
|
+
agents: {
|
|
24649
|
+
...local.agents,
|
|
24650
|
+
async register(input, context) {
|
|
24651
|
+
const result = await local.agents.register(input, context);
|
|
24652
|
+
if (isAgent(result))
|
|
24653
|
+
mirror.enqueueUpsert("agents", result, context);
|
|
24654
|
+
return result;
|
|
24655
|
+
},
|
|
24656
|
+
async update(id, input, context) {
|
|
24657
|
+
const agent = await local.agents.update(id, input, context);
|
|
24658
|
+
if (agent)
|
|
24659
|
+
mirror.enqueueUpsert("agents", agent, context);
|
|
24660
|
+
return agent;
|
|
24661
|
+
}
|
|
24662
|
+
},
|
|
24663
|
+
taskLists: {
|
|
24664
|
+
...local.taskLists,
|
|
24665
|
+
async create(input, context) {
|
|
24666
|
+
const list = await local.taskLists.create(input, context);
|
|
24667
|
+
mirror.enqueueUpsert("taskLists", list, context);
|
|
24668
|
+
return list;
|
|
24669
|
+
},
|
|
24670
|
+
async update(id, input, context) {
|
|
24671
|
+
const list = await local.taskLists.update(id, input, context);
|
|
24672
|
+
mirror.enqueueUpsert("taskLists", list, context);
|
|
24673
|
+
return list;
|
|
24674
|
+
},
|
|
24675
|
+
async delete(id, context) {
|
|
24676
|
+
const deleted = await local.taskLists.delete(id, context);
|
|
24677
|
+
if (deleted)
|
|
24678
|
+
mirror.enqueueDelete("task_lists", id, context);
|
|
24679
|
+
return deleted;
|
|
24680
|
+
}
|
|
24681
|
+
},
|
|
24682
|
+
templates: {
|
|
24683
|
+
...local.templates,
|
|
24684
|
+
async create(input, context) {
|
|
24685
|
+
const template = await local.templates.create(input, context);
|
|
24686
|
+
mirror.enqueueUpsert("templates", template, context);
|
|
24687
|
+
return template;
|
|
24688
|
+
},
|
|
24689
|
+
async update(id, input, context) {
|
|
24690
|
+
const template = await local.templates.update(id, input, context);
|
|
24691
|
+
if (template)
|
|
24692
|
+
mirror.enqueueUpsert("templates", template, context);
|
|
24693
|
+
return template;
|
|
24694
|
+
},
|
|
24695
|
+
async delete(id, context) {
|
|
24696
|
+
const deleted = await local.templates.delete(id, context);
|
|
24697
|
+
if (deleted)
|
|
24698
|
+
mirror.enqueueDelete("templates", id, context);
|
|
24699
|
+
return deleted;
|
|
24700
|
+
}
|
|
24701
|
+
},
|
|
24702
|
+
audit: {
|
|
24703
|
+
...local.audit,
|
|
24704
|
+
async logTaskChange(taskId, action, field2, oldValue, newValue, agentId, context) {
|
|
24705
|
+
const history = await local.audit.logTaskChange(taskId, action, field2, oldValue, newValue, agentId, context);
|
|
24706
|
+
mirror.enqueueUpsert("auditHistory", history, context);
|
|
24707
|
+
return history;
|
|
24708
|
+
}
|
|
24709
|
+
},
|
|
24710
|
+
shadow: mirror
|
|
24711
|
+
};
|
|
24712
|
+
if (adapter.transaction) {
|
|
24713
|
+
const localTransaction = local.transaction;
|
|
24714
|
+
adapter.transaction = (fn, context) => localTransaction(() => fn(adapter), context);
|
|
24715
|
+
}
|
|
24716
|
+
return adapter;
|
|
24717
|
+
}
|
|
24718
|
+
function isAgent(value) {
|
|
24719
|
+
return !value.conflict;
|
|
24720
|
+
}
|
|
24721
|
+
|
|
24722
|
+
// src/storage/cloud-client.ts
|
|
24723
|
+
init_config();
|
|
24724
|
+
function resolveBunSql() {
|
|
24725
|
+
const runtime = globalThis.Bun;
|
|
24726
|
+
const ctor = runtime?.SQL;
|
|
24727
|
+
if (typeof ctor !== "function") {
|
|
24728
|
+
throw new Error("Live Postgres access requires the Bun runtime (Bun.SQL). Run todos under bun, or inject a TodosPostgresQueryClient.");
|
|
24729
|
+
}
|
|
24730
|
+
return ctor;
|
|
24731
|
+
}
|
|
24732
|
+
function toRows(result) {
|
|
24733
|
+
if (Array.isArray(result))
|
|
24734
|
+
return result;
|
|
24735
|
+
if (result && typeof result === "object" && Array.isArray(result.rows)) {
|
|
24736
|
+
return result.rows;
|
|
24737
|
+
}
|
|
24738
|
+
return [];
|
|
24739
|
+
}
|
|
24740
|
+
function createTodosCloudQueryClient(url, options = {}) {
|
|
24741
|
+
const SQL = resolveBunSql();
|
|
24742
|
+
const sql = new SQL(url, {
|
|
24743
|
+
max: options.max ?? 2,
|
|
24744
|
+
idleTimeout: options.idleTimeout ?? 20,
|
|
24745
|
+
connectionTimeout: options.connectionTimeout ?? 15
|
|
24746
|
+
});
|
|
24747
|
+
return {
|
|
24748
|
+
async query(text, values = []) {
|
|
24749
|
+
const result = await sql.unsafe(text, values.length ? [...values] : []);
|
|
24750
|
+
return { rows: toRows(result) };
|
|
24751
|
+
},
|
|
24752
|
+
async close() {
|
|
24753
|
+
if (typeof sql.end === "function")
|
|
24754
|
+
await sql.end();
|
|
24755
|
+
else if (typeof sql.close === "function")
|
|
24756
|
+
await sql.close();
|
|
24757
|
+
}
|
|
24758
|
+
};
|
|
24759
|
+
}
|
|
24760
|
+
function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
24761
|
+
const url = getTodosStorageDatabaseUrl(env);
|
|
24762
|
+
if (!url)
|
|
24763
|
+
return null;
|
|
24764
|
+
return createTodosCloudQueryClient(url, options);
|
|
24765
|
+
}
|
|
24766
|
+
|
|
24181
24767
|
// src/storage/factory.ts
|
|
24768
|
+
init_config();
|
|
24182
24769
|
function createTodosStorageAdapter(options = {}) {
|
|
24183
24770
|
const config = options.config ?? loadTodosStorageConfig(options.env);
|
|
24184
|
-
if (config.mode === "local")
|
|
24771
|
+
if (config.mode === "local") {
|
|
24772
|
+
if (isTodosShadowEnabled(options.env))
|
|
24773
|
+
return createShadowAdapter(options, config);
|
|
24185
24774
|
return createLocalSqliteTodosStorageAdapter(options.local);
|
|
24775
|
+
}
|
|
24186
24776
|
assertTodosRemoteStorageConfig(config);
|
|
24187
24777
|
const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
|
|
24188
24778
|
if (!adapter) {
|
|
@@ -24191,6 +24781,20 @@ function createTodosStorageAdapter(options = {}) {
|
|
|
24191
24781
|
assertRemoteAdapterCapabilities(adapter, config.mode);
|
|
24192
24782
|
return adapter;
|
|
24193
24783
|
}
|
|
24784
|
+
function createShadowAdapter(options, config) {
|
|
24785
|
+
assertTodosShadowConfig(config, options.env);
|
|
24786
|
+
const postgresClient = options.postgresClient ?? createTodosCloudQueryClientFromEnv(options.env);
|
|
24787
|
+
if (!options.postgresSyncStore && !postgresClient) {
|
|
24788
|
+
throw new Error("shadow mirror requires a remote database URL or an injected Postgres client");
|
|
24789
|
+
}
|
|
24790
|
+
return createShadowTodosStorageAdapter({
|
|
24791
|
+
...options.shadow ?? {},
|
|
24792
|
+
...options.local ? { local: options.local } : {},
|
|
24793
|
+
...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
|
|
24794
|
+
...postgresClient ? { postgresClient } : {},
|
|
24795
|
+
...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
|
|
24796
|
+
});
|
|
24797
|
+
}
|
|
24194
24798
|
function createImplicitPostgresAdapter(options) {
|
|
24195
24799
|
if (!options.postgresClient)
|
|
24196
24800
|
return null;
|
|
@@ -24217,6 +24821,326 @@ function assertRemoteAdapterCapabilities(adapter, mode) {
|
|
|
24217
24821
|
throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
|
|
24218
24822
|
}
|
|
24219
24823
|
}
|
|
24824
|
+
// src/storage/shadow-outbox.ts
|
|
24825
|
+
init_shadow_outbox_schema();
|
|
24826
|
+
init_shadow_outbox_schema();
|
|
24827
|
+
var MAX_BACKOFF_MS = 5 * 60000;
|
|
24828
|
+
|
|
24829
|
+
class TodosShadowOutbox {
|
|
24830
|
+
db;
|
|
24831
|
+
syncStore;
|
|
24832
|
+
local;
|
|
24833
|
+
maxRetries;
|
|
24834
|
+
retryBaseMs;
|
|
24835
|
+
batchSize;
|
|
24836
|
+
onEvent;
|
|
24837
|
+
schemaReady;
|
|
24838
|
+
loopTimer = null;
|
|
24839
|
+
draining = false;
|
|
24840
|
+
metrics = {
|
|
24841
|
+
mirrored: 0,
|
|
24842
|
+
retries: 0,
|
|
24843
|
+
lastMirrorAt: null,
|
|
24844
|
+
lastLagMs: null,
|
|
24845
|
+
lastError: null
|
|
24846
|
+
};
|
|
24847
|
+
constructor(options) {
|
|
24848
|
+
this.db = options.db;
|
|
24849
|
+
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, {
|
|
24850
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
24851
|
+
}) : null);
|
|
24852
|
+
if (!syncStore) {
|
|
24853
|
+
throw new Error("shadow outbox requires a Postgres sync store or query client");
|
|
24854
|
+
}
|
|
24855
|
+
this.syncStore = syncStore;
|
|
24856
|
+
this.local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter({ db: options.db });
|
|
24857
|
+
this.maxRetries = Math.max(0, options.maxRetries ?? 8);
|
|
24858
|
+
this.retryBaseMs = Math.max(1, options.retryBaseMs ?? 500);
|
|
24859
|
+
this.batchSize = Math.max(1, options.batchSize ?? 50);
|
|
24860
|
+
if (options.onEvent)
|
|
24861
|
+
this.onEvent = options.onEvent;
|
|
24862
|
+
this.schemaReady = options.ensureSchema === false ? Promise.resolve() : null;
|
|
24863
|
+
}
|
|
24864
|
+
install() {
|
|
24865
|
+
installShadowOutboxSchema(this.db);
|
|
24866
|
+
}
|
|
24867
|
+
getStats() {
|
|
24868
|
+
const pending = this.countByStatus("pending");
|
|
24869
|
+
const failed = this.countByStatus("failed");
|
|
24870
|
+
return {
|
|
24871
|
+
enabled: true,
|
|
24872
|
+
durable: true,
|
|
24873
|
+
pending,
|
|
24874
|
+
failed,
|
|
24875
|
+
depth: pending + failed,
|
|
24876
|
+
mirrored: this.metrics.mirrored,
|
|
24877
|
+
retries: this.metrics.retries,
|
|
24878
|
+
lastMirrorAt: this.metrics.lastMirrorAt,
|
|
24879
|
+
lastLagMs: this.metrics.lastLagMs,
|
|
24880
|
+
lastError: this.metrics.lastError,
|
|
24881
|
+
draining: this.loopTimer !== null
|
|
24882
|
+
};
|
|
24883
|
+
}
|
|
24884
|
+
countByStatus(status) {
|
|
24885
|
+
const row = this.db.query(`SELECT count(*) AS n FROM shadow_outbox WHERE status=?`).get(status);
|
|
24886
|
+
return row?.n ?? 0;
|
|
24887
|
+
}
|
|
24888
|
+
startLoop(intervalMs = 2000) {
|
|
24889
|
+
if (this.loopTimer)
|
|
24890
|
+
return;
|
|
24891
|
+
this.loopTimer = setInterval(() => {
|
|
24892
|
+
this.drainOnce().catch(() => {});
|
|
24893
|
+
}, Math.max(250, intervalMs));
|
|
24894
|
+
if (typeof this.loopTimer.unref === "function")
|
|
24895
|
+
this.loopTimer.unref();
|
|
24896
|
+
}
|
|
24897
|
+
stopLoop() {
|
|
24898
|
+
if (this.loopTimer) {
|
|
24899
|
+
clearInterval(this.loopTimer);
|
|
24900
|
+
this.loopTimer = null;
|
|
24901
|
+
}
|
|
24902
|
+
}
|
|
24903
|
+
async flush(timeoutMs = 15000) {
|
|
24904
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
24905
|
+
while (Date.now() < deadline) {
|
|
24906
|
+
const processed = await this.drainOnce();
|
|
24907
|
+
if (this.countByStatus("pending") === 0)
|
|
24908
|
+
break;
|
|
24909
|
+
if (processed === 0) {
|
|
24910
|
+
const remaining = deadline - Date.now();
|
|
24911
|
+
if (remaining <= 0)
|
|
24912
|
+
break;
|
|
24913
|
+
await new Promise((resolve10) => setTimeout(resolve10, Math.min(200, remaining)));
|
|
24914
|
+
}
|
|
24915
|
+
}
|
|
24916
|
+
return this.getStats();
|
|
24917
|
+
}
|
|
24918
|
+
async drainOnce() {
|
|
24919
|
+
if (this.draining)
|
|
24920
|
+
return 0;
|
|
24921
|
+
this.draining = true;
|
|
24922
|
+
try {
|
|
24923
|
+
await this.ensureSchema();
|
|
24924
|
+
const now4 = Date.now();
|
|
24925
|
+
const rows = this.db.query(`SELECT seq, object_type, object_id, op, enqueued_at, attempts, revision
|
|
24926
|
+
FROM shadow_outbox
|
|
24927
|
+
WHERE status='pending' AND next_attempt_at <= ?
|
|
24928
|
+
ORDER BY seq ASC
|
|
24929
|
+
LIMIT ?`).all(now4, this.batchSize);
|
|
24930
|
+
if (rows.length === 0)
|
|
24931
|
+
return 0;
|
|
24932
|
+
for (const row of rows) {
|
|
24933
|
+
await this.processRow(row);
|
|
24934
|
+
}
|
|
24935
|
+
return rows.length;
|
|
24936
|
+
} catch {
|
|
24937
|
+
return 0;
|
|
24938
|
+
} finally {
|
|
24939
|
+
this.draining = false;
|
|
24940
|
+
}
|
|
24941
|
+
}
|
|
24942
|
+
async processRow(row) {
|
|
24943
|
+
try {
|
|
24944
|
+
const snapshot = await this.buildSnapshot(row);
|
|
24945
|
+
await this.syncStore.pushSnapshot(snapshot, {});
|
|
24946
|
+
const lagMs = Date.now() - row.enqueued_at;
|
|
24947
|
+
this.db.run(`DELETE FROM shadow_outbox WHERE seq=? AND revision=?`, [row.seq, row.revision]);
|
|
24948
|
+
this.metrics.mirrored += 1;
|
|
24949
|
+
this.metrics.lastMirrorAt = new Date().toISOString();
|
|
24950
|
+
this.metrics.lastLagMs = lagMs;
|
|
24951
|
+
this.onEvent?.({ type: "mirrored", objectType: row.object_type, id: row.object_id, lagMs });
|
|
24952
|
+
} catch (error) {
|
|
24953
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24954
|
+
this.metrics.retries += 1;
|
|
24955
|
+
this.metrics.lastError = message;
|
|
24956
|
+
const attempts = row.attempts + 1;
|
|
24957
|
+
if (attempts > this.maxRetries) {
|
|
24958
|
+
this.db.run(`UPDATE shadow_outbox SET attempts=?, last_error=?, status='failed' WHERE seq=? AND revision=?`, [attempts, message, row.seq, row.revision]);
|
|
24959
|
+
this.onEvent?.({ type: "parked", objectType: row.object_type, id: row.object_id, error: message });
|
|
24960
|
+
return;
|
|
24961
|
+
}
|
|
24962
|
+
const delay = Math.min(MAX_BACKOFF_MS, this.retryBaseMs * 2 ** (attempts - 1));
|
|
24963
|
+
this.db.run(`UPDATE shadow_outbox SET attempts=?, next_attempt_at=?, last_error=? WHERE seq=? AND revision=?`, [attempts, Date.now() + delay, message, row.seq, row.revision]);
|
|
24964
|
+
this.onEvent?.({ type: "retry", objectType: row.object_type, id: row.object_id, attempt: attempts, error: message });
|
|
24965
|
+
}
|
|
24966
|
+
}
|
|
24967
|
+
async buildSnapshot(row) {
|
|
24968
|
+
const snapshot = emptySnapshot2();
|
|
24969
|
+
if (row.op === "delete") {
|
|
24970
|
+
snapshot.tombstones = [this.tombstone(row.object_type, row.object_id)];
|
|
24971
|
+
return snapshot;
|
|
24972
|
+
}
|
|
24973
|
+
const record = await this.readCurrent(row.object_type, row.object_id);
|
|
24974
|
+
if (!record) {
|
|
24975
|
+
snapshot.tombstones = [this.tombstone(row.object_type, row.object_id)];
|
|
24976
|
+
return snapshot;
|
|
24977
|
+
}
|
|
24978
|
+
switch (row.object_type) {
|
|
24979
|
+
case "tasks":
|
|
24980
|
+
snapshot.tasks.push(record);
|
|
24981
|
+
break;
|
|
24982
|
+
case "projects":
|
|
24983
|
+
snapshot.projects.push(record);
|
|
24984
|
+
break;
|
|
24985
|
+
case "project_machine_paths":
|
|
24986
|
+
snapshot.projectMachinePaths?.push(record);
|
|
24987
|
+
break;
|
|
24988
|
+
case "plans":
|
|
24989
|
+
snapshot.plans.push(record);
|
|
24990
|
+
break;
|
|
24991
|
+
case "agents":
|
|
24992
|
+
snapshot.agents.push(record);
|
|
24993
|
+
break;
|
|
24994
|
+
case "task_lists":
|
|
24995
|
+
snapshot.taskLists.push(record);
|
|
24996
|
+
break;
|
|
24997
|
+
case "templates":
|
|
24998
|
+
snapshot.templates.push(record);
|
|
24999
|
+
break;
|
|
25000
|
+
case "audit_history":
|
|
25001
|
+
snapshot.auditHistory.push(record);
|
|
25002
|
+
break;
|
|
25003
|
+
default:
|
|
25004
|
+
break;
|
|
25005
|
+
}
|
|
25006
|
+
return snapshot;
|
|
25007
|
+
}
|
|
25008
|
+
async readCurrent(objectType, id) {
|
|
25009
|
+
switch (objectType) {
|
|
25010
|
+
case "tasks":
|
|
25011
|
+
return await this.local.tasks.get(id);
|
|
25012
|
+
case "projects":
|
|
25013
|
+
return await this.local.projects.get(id);
|
|
25014
|
+
case "project_machine_paths": {
|
|
25015
|
+
const raw = this.db.query(`SELECT * FROM project_machine_paths WHERE id=?`).get(id);
|
|
25016
|
+
return raw ?? null;
|
|
25017
|
+
}
|
|
25018
|
+
case "plans":
|
|
25019
|
+
return await this.local.plans.get(id);
|
|
25020
|
+
case "agents":
|
|
25021
|
+
return await this.local.agents.get(id);
|
|
25022
|
+
case "task_lists":
|
|
25023
|
+
return await this.local.taskLists.get(id);
|
|
25024
|
+
case "templates":
|
|
25025
|
+
return await this.local.templates.get(id);
|
|
25026
|
+
case "audit_history": {
|
|
25027
|
+
const raw = this.db.query(`SELECT * FROM task_history WHERE id=?`).get(id);
|
|
25028
|
+
return raw ?? null;
|
|
25029
|
+
}
|
|
25030
|
+
default:
|
|
25031
|
+
return null;
|
|
25032
|
+
}
|
|
25033
|
+
}
|
|
25034
|
+
tombstone(objectType, id) {
|
|
25035
|
+
const now4 = new Date().toISOString();
|
|
25036
|
+
return {
|
|
25037
|
+
object_type: objectType,
|
|
25038
|
+
object_id: id,
|
|
25039
|
+
deleted_at: now4,
|
|
25040
|
+
updated_at: now4,
|
|
25041
|
+
payload: { id, deleted_at: now4 },
|
|
25042
|
+
version: null
|
|
25043
|
+
};
|
|
25044
|
+
}
|
|
25045
|
+
ensureSchema() {
|
|
25046
|
+
if (!this.schemaReady) {
|
|
25047
|
+
this.schemaReady = this.syncStore.ensureSchema().catch((error) => {
|
|
25048
|
+
this.schemaReady = null;
|
|
25049
|
+
throw error;
|
|
25050
|
+
});
|
|
25051
|
+
}
|
|
25052
|
+
return this.schemaReady;
|
|
25053
|
+
}
|
|
25054
|
+
}
|
|
25055
|
+
function emptySnapshot2() {
|
|
25056
|
+
return {
|
|
25057
|
+
exportedAt: new Date().toISOString(),
|
|
25058
|
+
source: "sqlite",
|
|
25059
|
+
tasks: [],
|
|
25060
|
+
projects: [],
|
|
25061
|
+
projectMachinePaths: [],
|
|
25062
|
+
plans: [],
|
|
25063
|
+
agents: [],
|
|
25064
|
+
taskLists: [],
|
|
25065
|
+
templates: [],
|
|
25066
|
+
auditHistory: [],
|
|
25067
|
+
tombstones: []
|
|
25068
|
+
};
|
|
25069
|
+
}
|
|
25070
|
+
function createTodosShadowOutbox(options) {
|
|
25071
|
+
const outbox = new TodosShadowOutbox(options);
|
|
25072
|
+
outbox.install();
|
|
25073
|
+
return outbox;
|
|
25074
|
+
}
|
|
25075
|
+
// src/storage/shadow-runtime.ts
|
|
25076
|
+
init_config();
|
|
25077
|
+
var _capturedDb = null;
|
|
25078
|
+
var _outbox = null;
|
|
25079
|
+
var _cloud = null;
|
|
25080
|
+
var _exitRegistered = false;
|
|
25081
|
+
function maybeInstallShadowCapture2(db, env = process.env) {
|
|
25082
|
+
if (!isTodosShadowEnabled(env))
|
|
25083
|
+
return false;
|
|
25084
|
+
if (_capturedDb === db)
|
|
25085
|
+
return true;
|
|
25086
|
+
installShadowOutboxSchema(db);
|
|
25087
|
+
_capturedDb = db;
|
|
25088
|
+
return true;
|
|
25089
|
+
}
|
|
25090
|
+
function getRuntimeShadowOutbox(db, env = process.env) {
|
|
25091
|
+
if (!isTodosShadowEnabled(env)) {
|
|
25092
|
+
throw new Error(`dual-write shadow is disabled: set ${getTodosStorageShadowEnvName(env)}=1 to enable it`);
|
|
25093
|
+
}
|
|
25094
|
+
if (_outbox && _capturedDb === db)
|
|
25095
|
+
return _outbox;
|
|
25096
|
+
const cloud = createTodosCloudQueryClientFromEnv(env);
|
|
25097
|
+
if (!cloud) {
|
|
25098
|
+
throw new Error(`dual-write shadow drain requires a remote Postgres DSN in ${getTodosStorageDatabaseEnv(env)}`);
|
|
25099
|
+
}
|
|
25100
|
+
_cloud = cloud;
|
|
25101
|
+
installShadowOutboxSchema(db);
|
|
25102
|
+
_capturedDb = db;
|
|
25103
|
+
_outbox = new TodosShadowOutbox({ db, postgresClient: cloud });
|
|
25104
|
+
return _outbox;
|
|
25105
|
+
}
|
|
25106
|
+
function startRuntimeShadowDrain(db, env = process.env) {
|
|
25107
|
+
if (!isTodosShadowEnabled(env))
|
|
25108
|
+
return;
|
|
25109
|
+
maybeInstallShadowCapture2(db, env);
|
|
25110
|
+
try {
|
|
25111
|
+
const outbox = getRuntimeShadowOutbox(db, env);
|
|
25112
|
+
outbox.startLoop();
|
|
25113
|
+
registerShadowExitFlush(db, env);
|
|
25114
|
+
} catch (error) {
|
|
25115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25116
|
+
console.error(`[todos] shadow drain not started: ${message}`);
|
|
25117
|
+
}
|
|
25118
|
+
}
|
|
25119
|
+
function registerShadowExitFlush(db, env = process.env) {
|
|
25120
|
+
if (_exitRegistered)
|
|
25121
|
+
return;
|
|
25122
|
+
if (!isTodosShadowEnabled(env))
|
|
25123
|
+
return;
|
|
25124
|
+
_exitRegistered = true;
|
|
25125
|
+
const flush = async () => {
|
|
25126
|
+
try {
|
|
25127
|
+
const outbox = getRuntimeShadowOutbox(db, env);
|
|
25128
|
+
await outbox.flush(5000);
|
|
25129
|
+
} catch {} finally {
|
|
25130
|
+
await closeRuntimeShadowCloud();
|
|
25131
|
+
}
|
|
25132
|
+
};
|
|
25133
|
+
process.once("beforeExit", () => {
|
|
25134
|
+
flush();
|
|
25135
|
+
});
|
|
25136
|
+
}
|
|
25137
|
+
async function closeRuntimeShadowCloud() {
|
|
25138
|
+
if (_cloud) {
|
|
25139
|
+
const cloud = _cloud;
|
|
25140
|
+
_cloud = null;
|
|
25141
|
+
await cloud.close().catch(() => {});
|
|
25142
|
+
}
|
|
25143
|
+
}
|
|
24220
25144
|
// src/storage/s3-artifacts.ts
|
|
24221
25145
|
import { createHash as createHash9, createHmac as createHmac2 } from "crypto";
|
|
24222
25146
|
function createTodosS3ArtifactStore(options) {
|
|
@@ -24697,7 +25621,7 @@ class TodosTimeoutError extends Error {
|
|
|
24697
25621
|
}
|
|
24698
25622
|
|
|
24699
25623
|
// src/sdk/client.ts
|
|
24700
|
-
|
|
25624
|
+
init_config2();
|
|
24701
25625
|
function buildQuery(params) {
|
|
24702
25626
|
const search = new URLSearchParams;
|
|
24703
25627
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -28558,7 +29482,7 @@ CLI equivalent: \`${r.equivalent_cli}\`
|
|
|
28558
29482
|
init_task_commits();
|
|
28559
29483
|
init_database();
|
|
28560
29484
|
init_tasks();
|
|
28561
|
-
|
|
29485
|
+
init_config2();
|
|
28562
29486
|
init_redaction();
|
|
28563
29487
|
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
|
|
28564
29488
|
var DEFAULT_RETRY = {
|
|
@@ -29028,7 +29952,7 @@ init_task_commits();
|
|
|
29028
29952
|
init_database();
|
|
29029
29953
|
init_tasks();
|
|
29030
29954
|
init_task_runs();
|
|
29031
|
-
|
|
29955
|
+
init_config2();
|
|
29032
29956
|
import { relative as relative3, resolve as resolve13 } from "path";
|
|
29033
29957
|
function normalizePath4(path) {
|
|
29034
29958
|
return resolve13(path);
|
|
@@ -29496,7 +30420,7 @@ init_task_runs();
|
|
|
29496
30420
|
init_database();
|
|
29497
30421
|
init_redaction();
|
|
29498
30422
|
init_runner_sandbox();
|
|
29499
|
-
|
|
30423
|
+
init_config2();
|
|
29500
30424
|
function dispatcherFromRun(run) {
|
|
29501
30425
|
const value = run.metadata["agent_run_dispatcher"];
|
|
29502
30426
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -41875,7 +42799,7 @@ function runSearchView(idOrName, db) {
|
|
|
41875
42799
|
}
|
|
41876
42800
|
// src/lib/claude-tasks.ts
|
|
41877
42801
|
init_tasks();
|
|
41878
|
-
|
|
42802
|
+
init_config2();
|
|
41879
42803
|
init_sync_utils();
|
|
41880
42804
|
import { existsSync as existsSync26, readFileSync as readFileSync23, readdirSync as readdirSync5, writeFileSync as writeFileSync20 } from "fs";
|
|
41881
42805
|
import { join as join24 } from "path";
|
|
@@ -42118,7 +43042,7 @@ function syncClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
42118
43042
|
// src/lib/agent-tasks.ts
|
|
42119
43043
|
init_tasks();
|
|
42120
43044
|
init_sync_utils();
|
|
42121
|
-
|
|
43045
|
+
init_config2();
|
|
42122
43046
|
import { existsSync as existsSync27 } from "fs";
|
|
42123
43047
|
import { join as join25 } from "path";
|
|
42124
43048
|
function agentBaseDir(agent) {
|
|
@@ -42330,7 +43254,7 @@ function syncAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
|
42330
43254
|
}
|
|
42331
43255
|
|
|
42332
43256
|
// src/lib/sync.ts
|
|
42333
|
-
|
|
43257
|
+
init_config2();
|
|
42334
43258
|
function normalizeAgent2(agent) {
|
|
42335
43259
|
return agent.trim().toLowerCase();
|
|
42336
43260
|
}
|
|
@@ -42950,7 +43874,7 @@ function checkBudget(agentId, db) {
|
|
|
42950
43874
|
}
|
|
42951
43875
|
|
|
42952
43876
|
// src/index.ts
|
|
42953
|
-
|
|
43877
|
+
init_config2();
|
|
42954
43878
|
init_runner_sandbox();
|
|
42955
43879
|
init_event_hooks();
|
|
42956
43880
|
|
|
@@ -43160,7 +44084,7 @@ ${line}`,
|
|
|
43160
44084
|
init_tasks();
|
|
43161
44085
|
init_types();
|
|
43162
44086
|
init_database();
|
|
43163
|
-
|
|
44087
|
+
init_config2();
|
|
43164
44088
|
var LOCAL_FIELDS_KEY2 = "local_fields";
|
|
43165
44089
|
var WORKFLOW_STATE_KEY = "workflow_state";
|
|
43166
44090
|
var DEFAULT_WORKFLOW_STATES = [
|
|
@@ -43621,7 +44545,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
|
|
|
43621
44545
|
`);
|
|
43622
44546
|
}
|
|
43623
44547
|
// src/lib/local-extensions.ts
|
|
43624
|
-
|
|
44548
|
+
init_config2();
|
|
43625
44549
|
import { createHash as createHash17, createVerify } from "crypto";
|
|
43626
44550
|
import { existsSync as existsSync29, readdirSync as readdirSync6, readFileSync as readFileSync26, statSync as statSync11 } from "fs";
|
|
43627
44551
|
import { basename as basename6, join as join27, resolve as resolve19 } from "path";
|
|
@@ -45729,7 +46653,7 @@ function runTodosDoctor(options = {}) {
|
|
|
45729
46653
|
// src/lib/capacity-forecasts.ts
|
|
45730
46654
|
init_tasks();
|
|
45731
46655
|
init_task_relations();
|
|
45732
|
-
|
|
46656
|
+
init_config2();
|
|
45733
46657
|
var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
|
|
45734
46658
|
function timestamp2() {
|
|
45735
46659
|
return new Date().toISOString();
|