engrm 0.4.43 → 0.4.44
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/dist/cli.js +260 -24
- package/dist/hooks/elicitation-result.js +22 -10
- package/dist/hooks/post-tool-use.js +99 -36
- package/dist/hooks/pre-compact.js +35 -11
- package/dist/hooks/sentinel.js +22 -10
- package/dist/hooks/session-start.js +158 -14
- package/dist/hooks/stop.js +178 -41
- package/dist/hooks/user-prompt-submit.js +48 -12
- package/dist/server.js +202 -27
- package/opencode/README.md +6 -6
- package/opencode/install-or-update-opencode-plugin.sh +7 -1
- package/opencode/opencode.example.json +2 -2
- package/package.json +1 -1
|
@@ -97,10 +97,11 @@ function readProjectConfigFile(directory) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
function detectProject(directory) {
|
|
100
|
-
const
|
|
100
|
+
const resolvedDirectory = resolve(directory);
|
|
101
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
101
102
|
if (remoteUrl) {
|
|
102
103
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
103
|
-
const repoRoot = getGitTopLevel(
|
|
104
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
104
105
|
return {
|
|
105
106
|
canonical_id: canonicalId,
|
|
106
107
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -108,21 +109,22 @@ function detectProject(directory) {
|
|
|
108
109
|
local_path: repoRoot
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
|
-
const configFile = readProjectConfigFile(
|
|
112
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
112
113
|
if (configFile) {
|
|
113
114
|
return {
|
|
114
115
|
canonical_id: configFile.project_id,
|
|
115
116
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
116
117
|
remote_url: null,
|
|
117
|
-
local_path:
|
|
118
|
+
local_path: resolvedDirectory
|
|
118
119
|
};
|
|
119
120
|
}
|
|
120
|
-
const dirName = basename(
|
|
121
|
+
const dirName = basename(resolvedDirectory);
|
|
122
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
121
123
|
return {
|
|
122
|
-
canonical_id: `local/${
|
|
123
|
-
name:
|
|
124
|
+
canonical_id: `local/${safeDirName}`,
|
|
125
|
+
name: safeDirName,
|
|
124
126
|
remote_url: null,
|
|
125
|
-
local_path:
|
|
127
|
+
local_path: resolvedDirectory
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
130
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -1625,7 +1627,19 @@ function parseJsonArray2(value) {
|
|
|
1625
1627
|
}
|
|
1626
1628
|
}
|
|
1627
1629
|
function compactLine(value) {
|
|
1628
|
-
|
|
1630
|
+
if (value === null || value === undefined)
|
|
1631
|
+
return null;
|
|
1632
|
+
let text;
|
|
1633
|
+
if (typeof value === "string") {
|
|
1634
|
+
text = value;
|
|
1635
|
+
} else {
|
|
1636
|
+
try {
|
|
1637
|
+
text = JSON.stringify(value);
|
|
1638
|
+
} catch {
|
|
1639
|
+
text = String(value);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
1629
1643
|
if (!trimmed)
|
|
1630
1644
|
return null;
|
|
1631
1645
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -3266,7 +3280,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
3266
3280
|
import { join as join3 } from "node:path";
|
|
3267
3281
|
import { homedir } from "node:os";
|
|
3268
3282
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
3269
|
-
var CLIENT_VERSION = "0.4.
|
|
3283
|
+
var CLIENT_VERSION = "0.4.44";
|
|
3270
3284
|
function hashFile(filePath) {
|
|
3271
3285
|
try {
|
|
3272
3286
|
if (!existsSync3(filePath))
|
|
@@ -3619,6 +3633,96 @@ function asTeams(value, fallback) {
|
|
|
3619
3633
|
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
3620
3634
|
}
|
|
3621
3635
|
|
|
3636
|
+
// src/sync/auth.ts
|
|
3637
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3638
|
+
|
|
3639
|
+
// src/storage/outbox.ts
|
|
3640
|
+
function getPendingEntries(db, limit = 50) {
|
|
3641
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3642
|
+
return db.db.query(`SELECT * FROM sync_outbox
|
|
3643
|
+
WHERE (status = 'pending')
|
|
3644
|
+
OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
|
|
3645
|
+
ORDER BY created_at_epoch ASC
|
|
3646
|
+
LIMIT ?`).all(now, limit);
|
|
3647
|
+
}
|
|
3648
|
+
function markSyncing(db, entryId) {
|
|
3649
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3650
|
+
db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
|
|
3651
|
+
}
|
|
3652
|
+
function markSynced(db, entryId) {
|
|
3653
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3654
|
+
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
|
|
3655
|
+
}
|
|
3656
|
+
function markFailed(db, entryId, error) {
|
|
3657
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3658
|
+
db.db.query(`UPDATE sync_outbox SET
|
|
3659
|
+
status = 'failed',
|
|
3660
|
+
retry_count = retry_count + 1,
|
|
3661
|
+
last_error = ?,
|
|
3662
|
+
next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
|
|
3663
|
+
WHERE id = ?`).run(error, now, entryId);
|
|
3664
|
+
}
|
|
3665
|
+
function classifyOutboxFailure(error) {
|
|
3666
|
+
const normalized = error.toLowerCase();
|
|
3667
|
+
if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
|
|
3668
|
+
return "auth";
|
|
3669
|
+
}
|
|
3670
|
+
if (normalized.includes("429") || normalized.includes("rate limit")) {
|
|
3671
|
+
return "rate_limit";
|
|
3672
|
+
}
|
|
3673
|
+
if (normalized.includes("timeout") || normalized.includes("abort")) {
|
|
3674
|
+
return "timeout";
|
|
3675
|
+
}
|
|
3676
|
+
if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
|
|
3677
|
+
return "network";
|
|
3678
|
+
}
|
|
3679
|
+
if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
|
|
3680
|
+
return "validation";
|
|
3681
|
+
}
|
|
3682
|
+
return "other";
|
|
3683
|
+
}
|
|
3684
|
+
function resetFailedEntries(db) {
|
|
3685
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3686
|
+
SET status = 'pending',
|
|
3687
|
+
retry_count = 0,
|
|
3688
|
+
last_error = NULL,
|
|
3689
|
+
next_retry_epoch = NULL
|
|
3690
|
+
WHERE status = 'failed'`).run();
|
|
3691
|
+
return result.changes;
|
|
3692
|
+
}
|
|
3693
|
+
function resetFailedEntriesMatching(db, predicate) {
|
|
3694
|
+
const rows = db.db.query(`SELECT id, last_error
|
|
3695
|
+
FROM sync_outbox
|
|
3696
|
+
WHERE status = 'failed'`).all();
|
|
3697
|
+
const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
|
|
3698
|
+
if (matchingIds.length === 0)
|
|
3699
|
+
return 0;
|
|
3700
|
+
const placeholders = matchingIds.map(() => "?").join(", ");
|
|
3701
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3702
|
+
SET status = 'pending',
|
|
3703
|
+
retry_count = 0,
|
|
3704
|
+
last_error = NULL,
|
|
3705
|
+
next_retry_epoch = NULL
|
|
3706
|
+
WHERE id IN (${placeholders})`).run(...matchingIds);
|
|
3707
|
+
return result.changes;
|
|
3708
|
+
}
|
|
3709
|
+
function resetSyncingEntries(db) {
|
|
3710
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3711
|
+
SET status = 'pending',
|
|
3712
|
+
next_retry_epoch = NULL
|
|
3713
|
+
WHERE status = 'syncing'`).run();
|
|
3714
|
+
return result.changes;
|
|
3715
|
+
}
|
|
3716
|
+
function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
3717
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
|
|
3718
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3719
|
+
SET status = 'pending',
|
|
3720
|
+
next_retry_epoch = NULL
|
|
3721
|
+
WHERE status = 'syncing'
|
|
3722
|
+
AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
|
|
3723
|
+
return result.changes;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3622
3726
|
// src/sync/auth.ts
|
|
3623
3727
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
3624
3728
|
function normalizeBaseUrl(url) {
|
|
@@ -3650,6 +3754,36 @@ function getBaseUrl(config) {
|
|
|
3650
3754
|
}
|
|
3651
3755
|
return null;
|
|
3652
3756
|
}
|
|
3757
|
+
function getAuthFingerprint(config) {
|
|
3758
|
+
const apiKey = getApiKey(config);
|
|
3759
|
+
const baseUrl = getBaseUrl(config);
|
|
3760
|
+
if (!apiKey || !baseUrl)
|
|
3761
|
+
return null;
|
|
3762
|
+
return createHash3("sha256").update(`${baseUrl}
|
|
3763
|
+
${apiKey}
|
|
3764
|
+
${config.namespace}
|
|
3765
|
+
${config.site_id}`).digest("hex");
|
|
3766
|
+
}
|
|
3767
|
+
function recoverOutboxAfterAuthChange(db, config) {
|
|
3768
|
+
const fingerprint = getAuthFingerprint(config);
|
|
3769
|
+
if (!fingerprint) {
|
|
3770
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
3771
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
3772
|
+
}
|
|
3773
|
+
const key = "sync_auth_fingerprint";
|
|
3774
|
+
const previous = db.getSyncState(key);
|
|
3775
|
+
const fingerprintChanged = previous !== fingerprint;
|
|
3776
|
+
if (!fingerprintChanged) {
|
|
3777
|
+
const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure(error) === "auth");
|
|
3778
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
3779
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
3780
|
+
}
|
|
3781
|
+
const failedReset = resetFailedEntries(db);
|
|
3782
|
+
const syncingReset = resetSyncingEntries(db);
|
|
3783
|
+
const staleSyncingReset = 0;
|
|
3784
|
+
db.setSyncState(key, fingerprint);
|
|
3785
|
+
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
3786
|
+
}
|
|
3653
3787
|
function buildSourceId(config, localId, type = "obs") {
|
|
3654
3788
|
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
3655
3789
|
}
|
|
@@ -4639,6 +4773,7 @@ function ensureObservationTypes(db) {
|
|
|
4639
4773
|
DROP TABLE observations;
|
|
4640
4774
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
4641
4775
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
4776
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
4642
4777
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
4643
4778
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
4644
4779
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -4771,7 +4906,7 @@ function getSchemaVersion(db) {
|
|
|
4771
4906
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
4772
4907
|
|
|
4773
4908
|
// src/storage/sqlite.ts
|
|
4774
|
-
import { createHash as
|
|
4909
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
4775
4910
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
4776
4911
|
function openDatabase(dbPath) {
|
|
4777
4912
|
if (IS_BUN) {
|
|
@@ -4839,6 +4974,7 @@ class MemDatabase {
|
|
|
4839
4974
|
this.db = openDatabase(dbPath);
|
|
4840
4975
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
4841
4976
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
4977
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
4842
4978
|
this.vecAvailable = this.loadVecExtension();
|
|
4843
4979
|
runMigrations(this.db);
|
|
4844
4980
|
ensureObservationTypes(this.db);
|
|
@@ -4860,8 +4996,16 @@ class MemDatabase {
|
|
|
4860
4996
|
this.db.close();
|
|
4861
4997
|
}
|
|
4862
4998
|
upsertProject(project) {
|
|
4999
|
+
const canonicalId = project.canonical_id?.trim();
|
|
5000
|
+
const name = project.name?.trim();
|
|
5001
|
+
if (!canonicalId) {
|
|
5002
|
+
throw new Error("Project canonical_id is required");
|
|
5003
|
+
}
|
|
5004
|
+
if (!name) {
|
|
5005
|
+
throw new Error("Project name is required");
|
|
5006
|
+
}
|
|
4863
5007
|
const now = Math.floor(Date.now() / 1000);
|
|
4864
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
5008
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
4865
5009
|
if (existing) {
|
|
4866
5010
|
this.db.query(`UPDATE projects SET
|
|
4867
5011
|
local_path = COALESCE(?, local_path),
|
|
@@ -4876,7 +5020,7 @@ class MemDatabase {
|
|
|
4876
5020
|
};
|
|
4877
5021
|
}
|
|
4878
5022
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
4879
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
5023
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
4880
5024
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
4881
5025
|
}
|
|
4882
5026
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -5552,7 +5696,7 @@ class MemDatabase {
|
|
|
5552
5696
|
}
|
|
5553
5697
|
}
|
|
5554
5698
|
function hashPrompt(prompt) {
|
|
5555
|
-
return
|
|
5699
|
+
return createHash4("sha256").update(prompt).digest("hex");
|
|
5556
5700
|
}
|
|
5557
5701
|
|
|
5558
5702
|
// src/hooks/common.ts
|
package/dist/hooks/stop.js
CHANGED
|
@@ -1165,6 +1165,7 @@ function ensureObservationTypes(db) {
|
|
|
1165
1165
|
DROP TABLE observations;
|
|
1166
1166
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
1167
1167
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
1168
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
1168
1169
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
1169
1170
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
1170
1171
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1365,6 +1366,7 @@ class MemDatabase {
|
|
|
1365
1366
|
this.db = openDatabase(dbPath);
|
|
1366
1367
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1367
1368
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1369
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1368
1370
|
this.vecAvailable = this.loadVecExtension();
|
|
1369
1371
|
runMigrations(this.db);
|
|
1370
1372
|
ensureObservationTypes(this.db);
|
|
@@ -1386,8 +1388,16 @@ class MemDatabase {
|
|
|
1386
1388
|
this.db.close();
|
|
1387
1389
|
}
|
|
1388
1390
|
upsertProject(project) {
|
|
1391
|
+
const canonicalId = project.canonical_id?.trim();
|
|
1392
|
+
const name = project.name?.trim();
|
|
1393
|
+
if (!canonicalId) {
|
|
1394
|
+
throw new Error("Project canonical_id is required");
|
|
1395
|
+
}
|
|
1396
|
+
if (!name) {
|
|
1397
|
+
throw new Error("Project name is required");
|
|
1398
|
+
}
|
|
1389
1399
|
const now = Math.floor(Date.now() / 1000);
|
|
1390
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
1400
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
1391
1401
|
if (existing) {
|
|
1392
1402
|
this.db.query(`UPDATE projects SET
|
|
1393
1403
|
local_path = COALESCE(?, local_path),
|
|
@@ -1402,7 +1412,7 @@ class MemDatabase {
|
|
|
1402
1412
|
};
|
|
1403
1413
|
}
|
|
1404
1414
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1405
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1415
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1406
1416
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1407
1417
|
}
|
|
1408
1418
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -2214,6 +2224,96 @@ function formatRiskTrafficLight(result) {
|
|
|
2214
2224
|
return `${icon} Risk: ${result.score}/100 (${result.level})`;
|
|
2215
2225
|
}
|
|
2216
2226
|
|
|
2227
|
+
// src/sync/auth.ts
|
|
2228
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2229
|
+
|
|
2230
|
+
// src/storage/outbox.ts
|
|
2231
|
+
function getPendingEntries(db, limit = 50) {
|
|
2232
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2233
|
+
return db.db.query(`SELECT * FROM sync_outbox
|
|
2234
|
+
WHERE (status = 'pending')
|
|
2235
|
+
OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
|
|
2236
|
+
ORDER BY created_at_epoch ASC
|
|
2237
|
+
LIMIT ?`).all(now, limit);
|
|
2238
|
+
}
|
|
2239
|
+
function markSyncing(db, entryId) {
|
|
2240
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2241
|
+
db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
|
|
2242
|
+
}
|
|
2243
|
+
function markSynced(db, entryId) {
|
|
2244
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2245
|
+
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
|
|
2246
|
+
}
|
|
2247
|
+
function markFailed(db, entryId, error) {
|
|
2248
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2249
|
+
db.db.query(`UPDATE sync_outbox SET
|
|
2250
|
+
status = 'failed',
|
|
2251
|
+
retry_count = retry_count + 1,
|
|
2252
|
+
last_error = ?,
|
|
2253
|
+
next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
|
|
2254
|
+
WHERE id = ?`).run(error, now, entryId);
|
|
2255
|
+
}
|
|
2256
|
+
function classifyOutboxFailure(error) {
|
|
2257
|
+
const normalized = error.toLowerCase();
|
|
2258
|
+
if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
|
|
2259
|
+
return "auth";
|
|
2260
|
+
}
|
|
2261
|
+
if (normalized.includes("429") || normalized.includes("rate limit")) {
|
|
2262
|
+
return "rate_limit";
|
|
2263
|
+
}
|
|
2264
|
+
if (normalized.includes("timeout") || normalized.includes("abort")) {
|
|
2265
|
+
return "timeout";
|
|
2266
|
+
}
|
|
2267
|
+
if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
|
|
2268
|
+
return "network";
|
|
2269
|
+
}
|
|
2270
|
+
if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
|
|
2271
|
+
return "validation";
|
|
2272
|
+
}
|
|
2273
|
+
return "other";
|
|
2274
|
+
}
|
|
2275
|
+
function resetFailedEntries(db) {
|
|
2276
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2277
|
+
SET status = 'pending',
|
|
2278
|
+
retry_count = 0,
|
|
2279
|
+
last_error = NULL,
|
|
2280
|
+
next_retry_epoch = NULL
|
|
2281
|
+
WHERE status = 'failed'`).run();
|
|
2282
|
+
return result.changes;
|
|
2283
|
+
}
|
|
2284
|
+
function resetFailedEntriesMatching(db, predicate) {
|
|
2285
|
+
const rows = db.db.query(`SELECT id, last_error
|
|
2286
|
+
FROM sync_outbox
|
|
2287
|
+
WHERE status = 'failed'`).all();
|
|
2288
|
+
const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
|
|
2289
|
+
if (matchingIds.length === 0)
|
|
2290
|
+
return 0;
|
|
2291
|
+
const placeholders = matchingIds.map(() => "?").join(", ");
|
|
2292
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2293
|
+
SET status = 'pending',
|
|
2294
|
+
retry_count = 0,
|
|
2295
|
+
last_error = NULL,
|
|
2296
|
+
next_retry_epoch = NULL
|
|
2297
|
+
WHERE id IN (${placeholders})`).run(...matchingIds);
|
|
2298
|
+
return result.changes;
|
|
2299
|
+
}
|
|
2300
|
+
function resetSyncingEntries(db) {
|
|
2301
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2302
|
+
SET status = 'pending',
|
|
2303
|
+
next_retry_epoch = NULL
|
|
2304
|
+
WHERE status = 'syncing'`).run();
|
|
2305
|
+
return result.changes;
|
|
2306
|
+
}
|
|
2307
|
+
function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
2308
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
|
|
2309
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2310
|
+
SET status = 'pending',
|
|
2311
|
+
next_retry_epoch = NULL
|
|
2312
|
+
WHERE status = 'syncing'
|
|
2313
|
+
AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
|
|
2314
|
+
return result.changes;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2217
2317
|
// src/sync/auth.ts
|
|
2218
2318
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
2219
2319
|
function normalizeBaseUrl(url) {
|
|
@@ -2245,6 +2345,36 @@ function getBaseUrl(config) {
|
|
|
2245
2345
|
}
|
|
2246
2346
|
return null;
|
|
2247
2347
|
}
|
|
2348
|
+
function getAuthFingerprint(config) {
|
|
2349
|
+
const apiKey = getApiKey(config);
|
|
2350
|
+
const baseUrl = getBaseUrl(config);
|
|
2351
|
+
if (!apiKey || !baseUrl)
|
|
2352
|
+
return null;
|
|
2353
|
+
return createHash3("sha256").update(`${baseUrl}
|
|
2354
|
+
${apiKey}
|
|
2355
|
+
${config.namespace}
|
|
2356
|
+
${config.site_id}`).digest("hex");
|
|
2357
|
+
}
|
|
2358
|
+
function recoverOutboxAfterAuthChange(db, config) {
|
|
2359
|
+
const fingerprint = getAuthFingerprint(config);
|
|
2360
|
+
if (!fingerprint) {
|
|
2361
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
2362
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
2363
|
+
}
|
|
2364
|
+
const key = "sync_auth_fingerprint";
|
|
2365
|
+
const previous = db.getSyncState(key);
|
|
2366
|
+
const fingerprintChanged = previous !== fingerprint;
|
|
2367
|
+
if (!fingerprintChanged) {
|
|
2368
|
+
const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure(error) === "auth");
|
|
2369
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
2370
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
2371
|
+
}
|
|
2372
|
+
const failedReset = resetFailedEntries(db);
|
|
2373
|
+
const syncingReset = resetSyncingEntries(db);
|
|
2374
|
+
const staleSyncingReset = 0;
|
|
2375
|
+
db.setSyncState(key, fingerprint);
|
|
2376
|
+
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
2377
|
+
}
|
|
2248
2378
|
function buildSourceId(config, localId, type = "obs") {
|
|
2249
2379
|
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
2250
2380
|
}
|
|
@@ -2378,32 +2508,6 @@ class VectorApiError extends Error {
|
|
|
2378
2508
|
}
|
|
2379
2509
|
}
|
|
2380
2510
|
|
|
2381
|
-
// src/storage/outbox.ts
|
|
2382
|
-
function getPendingEntries(db, limit = 50) {
|
|
2383
|
-
const now = Math.floor(Date.now() / 1000);
|
|
2384
|
-
return db.db.query(`SELECT * FROM sync_outbox
|
|
2385
|
-
WHERE (status = 'pending')
|
|
2386
|
-
OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
|
|
2387
|
-
ORDER BY created_at_epoch ASC
|
|
2388
|
-
LIMIT ?`).all(now, limit);
|
|
2389
|
-
}
|
|
2390
|
-
function markSyncing(db, entryId) {
|
|
2391
|
-
db.db.query("UPDATE sync_outbox SET status = 'syncing' WHERE id = ?").run(entryId);
|
|
2392
|
-
}
|
|
2393
|
-
function markSynced(db, entryId) {
|
|
2394
|
-
const now = Math.floor(Date.now() / 1000);
|
|
2395
|
-
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ? WHERE id = ?").run(now, entryId);
|
|
2396
|
-
}
|
|
2397
|
-
function markFailed(db, entryId, error) {
|
|
2398
|
-
const now = Math.floor(Date.now() / 1000);
|
|
2399
|
-
db.db.query(`UPDATE sync_outbox SET
|
|
2400
|
-
status = 'failed',
|
|
2401
|
-
retry_count = retry_count + 1,
|
|
2402
|
-
last_error = ?,
|
|
2403
|
-
next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
|
|
2404
|
-
WHERE id = ?`).run(error, now, entryId);
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
2511
|
// src/intelligence/value-signals.ts
|
|
2408
2512
|
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
2409
2513
|
function computeSessionValueSignals(observations, securityFindings = []) {
|
|
@@ -2484,7 +2588,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
|
|
|
2484
2588
|
return null;
|
|
2485
2589
|
}
|
|
2486
2590
|
function compactLine(value) {
|
|
2487
|
-
|
|
2591
|
+
if (value === null || value === undefined)
|
|
2592
|
+
return null;
|
|
2593
|
+
let text;
|
|
2594
|
+
if (typeof value === "string") {
|
|
2595
|
+
text = value;
|
|
2596
|
+
} else {
|
|
2597
|
+
try {
|
|
2598
|
+
text = JSON.stringify(value);
|
|
2599
|
+
} catch {
|
|
2600
|
+
text = String(value);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
2488
2604
|
if (!trimmed)
|
|
2489
2605
|
return null;
|
|
2490
2606
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -3015,13 +3131,20 @@ function summarizeObservationSourceTools(observations) {
|
|
|
3015
3131
|
}
|
|
3016
3132
|
|
|
3017
3133
|
// src/sync/push-once.ts
|
|
3134
|
+
function recordNowEpoch(db, key) {
|
|
3135
|
+
db.setSyncState(key, String(Math.floor(Date.now() / 1000)));
|
|
3136
|
+
}
|
|
3018
3137
|
async function pushOnce(db, config, options = {}) {
|
|
3019
3138
|
if (!config.sync.enabled)
|
|
3020
3139
|
return 0;
|
|
3021
3140
|
if (!VectorClient.isConfigured(config))
|
|
3022
3141
|
return 0;
|
|
3023
3142
|
try {
|
|
3143
|
+
recoverOutboxAfterAuthChange(db, config);
|
|
3024
3144
|
const result = await pushOutbox(db, config, config.sync.batch_size, { timeoutMs: options.timeoutMs ?? 4000 });
|
|
3145
|
+
if (result.pushed > 0) {
|
|
3146
|
+
recordNowEpoch(db, "last_push_epoch");
|
|
3147
|
+
}
|
|
3025
3148
|
return result.pushed;
|
|
3026
3149
|
} catch {
|
|
3027
3150
|
return 0;
|
|
@@ -3245,7 +3368,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
3245
3368
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
3246
3369
|
risk_score: riskScore,
|
|
3247
3370
|
stacks_detected: stacks,
|
|
3248
|
-
client_version: "0.4.
|
|
3371
|
+
client_version: "0.4.44",
|
|
3249
3372
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3250
3373
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3251
3374
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3377,10 +3500,11 @@ function readProjectConfigFile(directory) {
|
|
|
3377
3500
|
}
|
|
3378
3501
|
}
|
|
3379
3502
|
function detectProject(directory) {
|
|
3380
|
-
const
|
|
3503
|
+
const resolvedDirectory = resolve(directory);
|
|
3504
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
3381
3505
|
if (remoteUrl) {
|
|
3382
3506
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
3383
|
-
const repoRoot = getGitTopLevel(
|
|
3507
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
3384
3508
|
return {
|
|
3385
3509
|
canonical_id: canonicalId,
|
|
3386
3510
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -3388,21 +3512,22 @@ function detectProject(directory) {
|
|
|
3388
3512
|
local_path: repoRoot
|
|
3389
3513
|
};
|
|
3390
3514
|
}
|
|
3391
|
-
const configFile = readProjectConfigFile(
|
|
3515
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
3392
3516
|
if (configFile) {
|
|
3393
3517
|
return {
|
|
3394
3518
|
canonical_id: configFile.project_id,
|
|
3395
3519
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
3396
3520
|
remote_url: null,
|
|
3397
|
-
local_path:
|
|
3521
|
+
local_path: resolvedDirectory
|
|
3398
3522
|
};
|
|
3399
3523
|
}
|
|
3400
|
-
const dirName = basename2(
|
|
3524
|
+
const dirName = basename2(resolvedDirectory);
|
|
3525
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
3401
3526
|
return {
|
|
3402
|
-
canonical_id: `local/${
|
|
3403
|
-
name:
|
|
3527
|
+
canonical_id: `local/${safeDirName}`,
|
|
3528
|
+
name: safeDirName,
|
|
3404
3529
|
remote_url: null,
|
|
3405
|
-
local_path:
|
|
3530
|
+
local_path: resolvedDirectory
|
|
3406
3531
|
};
|
|
3407
3532
|
}
|
|
3408
3533
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -3433,7 +3558,7 @@ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
|
3433
3558
|
}
|
|
3434
3559
|
|
|
3435
3560
|
// src/capture/transcript.ts
|
|
3436
|
-
import { createHash as
|
|
3561
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3437
3562
|
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3438
3563
|
import { join as join5 } from "node:path";
|
|
3439
3564
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -4230,7 +4355,7 @@ function dedupeHistoryMessages(messages) {
|
|
|
4230
4355
|
return deduped;
|
|
4231
4356
|
}
|
|
4232
4357
|
function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
4233
|
-
const digest =
|
|
4358
|
+
const digest = createHash4("sha1").update(text).digest("hex").slice(0, 12);
|
|
4234
4359
|
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
4235
4360
|
}
|
|
4236
4361
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
@@ -4741,7 +4866,19 @@ function parseJsonArray4(value) {
|
|
|
4741
4866
|
}
|
|
4742
4867
|
}
|
|
4743
4868
|
function compactLine2(value) {
|
|
4744
|
-
|
|
4869
|
+
if (value === null || value === undefined)
|
|
4870
|
+
return null;
|
|
4871
|
+
let text;
|
|
4872
|
+
if (typeof value === "string") {
|
|
4873
|
+
text = value;
|
|
4874
|
+
} else {
|
|
4875
|
+
try {
|
|
4876
|
+
text = JSON.stringify(value);
|
|
4877
|
+
} catch {
|
|
4878
|
+
text = String(value);
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
4745
4882
|
if (!trimmed)
|
|
4746
4883
|
return null;
|
|
4747
4884
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|