Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0078331
fix(gateway): close streams before post-response work
BYK Aug 11, 2026
6cf0891
fix(core): harden committed project resolution
BYK Aug 14, 2026
b649d7a
fix(gateway): harden affinity rotation authorization
BYK Aug 15, 2026
97d40ee
fix(gateway): require persisted owner for session adoption
BYK Aug 15, 2026
c32133c
fix(gateway): complete legacy adoption migration
BYK Aug 15, 2026
94ee90c
fix(gateway): serialize legacy session ownership
BYK Aug 15, 2026
6d39dd0
fix(gateway): fail closed on stale session identities
BYK Aug 15, 2026
aa734f4
fix(gateway): account only committed provisional turns
BYK Aug 15, 2026
13233b4
fix(cli): fail closed on unsafe upstream adoption
BYK Aug 15, 2026
098514f
fix(gateway): preserve incomplete Responses terminals
BYK Aug 15, 2026
d5b58d2
fix(gateway): validate incomplete Responses transport
BYK Aug 15, 2026
5cde66f
fix(gateway): validate public incomplete reasons
BYK Aug 15, 2026
bc609e2
fix(gateway): unify Responses terminal validation
BYK Aug 15, 2026
93105cb
fix(gateway): close Responses validation gaps
BYK Aug 15, 2026
712a287
fix(gateway): align Responses terminal cancellation
BYK Aug 15, 2026
73efd48
fix(cli): reject mapped IPv6 loopback upstreams
BYK Aug 15, 2026
15db393
fix(gateway): validate Responses snapshots
BYK Aug 15, 2026
280107c
fix(gateway): enforce terminal snapshot parity
BYK Aug 15, 2026
63bab48
fix(gateway): validate all Responses output items
BYK Aug 15, 2026
b6df2dc
fix(gateway): harden Responses terminal lifecycle
BYK Aug 15, 2026
4e8b475
merge: integrate Responses lifecycle hardening
BYK Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"development": "./src/index.ts",
"bun": "./dist/bun/index.js",
"default": "./dist/node/index.js"
}
Expand Down
34 changes: 31 additions & 3 deletions packages/core/src/curator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ export async function run(input: {
projectPath: string;
sessionID: string;
model?: { providerID: string; modelID: string };
signal?: AbortSignal;
/** Optional gateway worker-health hook — called when the LLM call returns
* null. The gateway uses this to escalate to Sentry after sustained failure. */
workerHealth?: {
Expand Down Expand Up @@ -707,7 +708,9 @@ export async function run(input: {
export async function dedupePreferenceCreates(
ops: CuratorOp[],
projectPath: string,
signal?: AbortSignal,
): Promise<CuratorOp[]> {
signal?.throwIfAborted();
if (!embedding.isAvailable()) return ops;
const pid = ensureProject(projectPath);
// Track ids already matched this batch so two paraphrases in the SAME op list
Expand All @@ -732,7 +735,9 @@ export async function dedupePreferenceCreates(
projectId: op.scope === "global" ? null : pid,
threshold: ltm.PREFERENCE_DEDUP_THRESHOLD,
});
signal?.throwIfAborted();
} catch (err) {
signal?.throwIfAborted();
log.warn(
"preference dedup: findSemanticDuplicate failed (non-fatal):",
err,
Expand Down Expand Up @@ -778,6 +783,7 @@ async function runInner(input: {
projectPath: string;
sessionID: string;
model?: { providerID: string; modelID: string };
signal?: AbortSignal;
workerHealth?: {
recordFailure(reason: string): void;
recordSuccess(): void;
Expand All @@ -792,6 +798,7 @@ async function runInner(input: {
relationsCreated: number;
changedEntries: ChangedEntry[];
}> {
input.signal?.throwIfAborted();
const cfg = config();

// Get recent undistilled messages since last curation.
Expand Down Expand Up @@ -887,7 +894,9 @@ async function runInner(input: {
projectPath: input.projectPath,
sessionID: input.sessionID,
});
input.signal?.throwIfAborted();
} catch (err) {
input.signal?.throwIfAborted();
log.warn("instruction-detect failed (non-fatal):", err);
}

Expand Down Expand Up @@ -949,7 +958,9 @@ async function runInner(input: {
sessionID: input.sessionID,
maxTokens: 2048,
temperature: 0,
signal: input.signal,
});
input.signal?.throwIfAborted();
if (!responseText) {
// Transport failure / empty completion already recorded by the LLM
// adapter (single owner of transport-failure attribution) — avoid
Expand Down Expand Up @@ -985,7 +996,12 @@ async function runInner(input: {
// embedding similarity at a preference-specific (looser) threshold to redirect
// a near-dup create onto the existing entry. Async embedding work lives here
// (runInner is async) so applyOps can stay synchronous.
const ops = await dedupePreferenceCreates(response.ops, input.projectPath);
const ops = await dedupePreferenceCreates(
response.ops,
input.projectPath,
input.signal,
);
input.signal?.throwIfAborted();

const result = applyOps(ops, {
projectPath: input.projectPath,
Expand All @@ -1003,7 +1019,9 @@ async function runInner(input: {
// similarity when available, falls back to word-overlap.
if (result.created > 0) {
try {
input.signal?.throwIfAborted();
const dupes = await ltm.deduplicate(input.projectPath, { dryRun: false });
input.signal?.throwIfAborted();
if (dupes.totalRemoved > 0) {
log.info(
`post-curation dedup: merged ${dupes.totalRemoved} duplicate entries`,
Expand All @@ -1023,6 +1041,7 @@ async function runInner(input: {
}
}
} catch (err) {
input.signal?.throwIfAborted();
log.warn("post-curation dedup failed (non-fatal):", err);
}

Expand All @@ -1034,13 +1053,15 @@ async function runInner(input: {
// callers that don't pre-check).
if (cfg.crossProject && embedding.isAvailable()) {
try {
input.signal?.throwIfAborted();
const promotion = ltm.promoteCrossProject({ dryRun: false });
if (promotion.promoted > 0) {
log.info(
`cross-project promotion: promoted ${promotion.promoted} entries across ${promotion.clusters.length} cluster(s)`,
);
}
} catch (err) {
input.signal?.throwIfAborted();
log.warn("cross-project promotion failed (non-fatal):", err);
}
}
Expand All @@ -1056,9 +1077,11 @@ async function runInner(input: {
// alias-overlap signals still fire, and the next run catches the rest.
if (result.entitiesCreated > 0 && embedding.isAvailable()) {
try {
input.signal?.throwIfAborted();
const dupes = await entities.deduplicateEntities(input.projectPath, {
dryRun: false,
});
input.signal?.throwIfAborted();
const autoMerged = dupes.merged.reduce((n, c) => n + c.merged.length, 0);
if (autoMerged > 0) {
log.info(
Expand All @@ -1077,12 +1100,16 @@ async function runInner(input: {
}
}
} catch (err) {
input.signal?.throwIfAborted();
log.warn("post-curation entity dedup failed (non-fatal):", err);
}
}

// Soft-cap enforcement: after creates and the dedup sweeps settle the count,
// evict the lowest-value project-scoped entries back down to maxEntries.
// The maintenance operations below are synchronous even though the dedup
// APIs return already-settled promises. Abort checks bracket every await and
// write phase so reset cannot resume a stale writer, while foreground runs
// still enforce the same duplicate, cap, and cursor invariants as idle runs.
input.signal?.throwIfAborted();
const evictedCount = enforceEntryCap(
input.projectPath,
cfg.curator.maxEntries,
Expand All @@ -1094,6 +1121,7 @@ async function runInner(input: {
);
}

input.signal?.throwIfAborted();
const now = Date.now();
lastCuratedAt.set(input.sessionID, now);
saveSessionTracking(input.sessionID, { lastCuratedAt: now });
Expand Down
25 changes: 21 additions & 4 deletions packages/core/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { statSync, unlinkSync, existsSync, rmSync } from "node:fs";
import { resolve as resolvePath } from "node:path";
import {
db,
databaseInTransaction,
ensureProject,
projectId,
projectPath as getProjectPathById,
Expand Down Expand Up @@ -156,10 +157,12 @@ onProjectMutation(() => {
/** List all projects with summary counts. */
export function listProjects(): ProjectSummary[] {
const now = Date.now();
if (projectsCache && now - projectsCacheAt < LIST_CACHE_TTL_MS) {
const database = db();
const cacheable = !databaseInTransaction(database);
if (cacheable && projectsCache && now - projectsCacheAt < LIST_CACHE_TTL_MS) {
return projectsCache;
}
const result = db()
const result = database
.query(
`SELECT p.id, p.path, p.name, p.git_remote, p.created_at,
COALESCE(k.cnt, 0) AS knowledge_count,
Expand Down Expand Up @@ -187,8 +190,10 @@ export function listProjects(): ProjectSummary[] {
ORDER BY p.created_at DESC`,
)
.all() as ProjectSummary[];
projectsCache = result;
projectsCacheAt = now;
if (cacheable) {
projectsCache = result;
projectsCacheAt = now;
}
return result;
}

Expand Down Expand Up @@ -881,6 +886,11 @@ export function deleteProject(projectId: string): ClearResult | null {
database
.query("DELETE FROM project_path_aliases WHERE project_id = ?")
.run(projectId);
database
.query(
"DELETE FROM project_id_aliases WHERE retired_id = ? OR project_id = ?",
)
.run(projectId, projectId);
database
.query("DELETE FROM warmup_histograms WHERE project_id = ?")
.run(projectId);
Expand Down Expand Up @@ -1604,6 +1614,13 @@ export type MergeResult = {
* Returns counts of moved rows for reporting.
*/
export function mergeProjects(sourceId: string, targetId: string): MergeResult {
if (sourceId === targetId) {
return {
knowledge_moved: 0,
messages_moved: 0,
distillations_moved: 0,
};
}
const database = db();

// Count before merging (result.changes is inflated by FTS triggers)
Expand Down
Loading
Loading