@aiwg/cli 2026.10.10 → 2026.10.11
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/agentic/code/providers/model-capabilities.v1.json +78 -14
- package/dist/src/cli/handlers/sessions.js +198 -15
- package/dist/src/cli/handlers/use.js +12 -4
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
- package/dist/src/models/model-capabilities.v1.json +78 -14
- package/dist/src/models/provider-policy.js +7 -0
- package/dist/src/policy/authorization.js +29 -1
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/batch-import.js +34 -6
- package/dist/src/sessions/contracts.js +19 -0
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +23 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/workspace-discovery.js +5 -2
- package/package.json +7 -1
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
- package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +51 -96
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Host catalog for account-scoped session providers (#3232).
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createSessionAdapterRegistry } from './adapter-registry.js';
|
|
5
|
+
/** Workspace id used for account-scoped sessions stored in the host catalog. */
|
|
6
|
+
export const HOST_CATALOG_WORKSPACE_ID = 'host';
|
|
7
|
+
/** DDL for the workspace-catalog reference table (created by SessionRepository). */
|
|
8
|
+
export const HOST_REFERENCE_TABLE_DDL = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS session_host_refs (
|
|
10
|
+
provider TEXT NOT NULL, native_session_id TEXT NOT NULL,
|
|
11
|
+
session_id TEXT NOT NULL, source_id TEXT NOT NULL,
|
|
12
|
+
host_catalog TEXT NOT NULL,
|
|
13
|
+
PRIMARY KEY(provider, native_session_id)
|
|
14
|
+
);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_session_host_refs_session
|
|
16
|
+
ON session_host_refs(session_id);
|
|
17
|
+
`;
|
|
18
|
+
const HOST_CATALOG_ENV = 'AIWG_SESSIONS_HOST_CATALOG';
|
|
19
|
+
/** User-scope (host) catalog path; the env override exists so tests never touch HOME. */
|
|
20
|
+
export function defaultHostCatalogPath(homeDir = homedir()) {
|
|
21
|
+
const override = process.env[HOST_CATALOG_ENV]?.trim();
|
|
22
|
+
if (override)
|
|
23
|
+
return override;
|
|
24
|
+
return join(homeDir, '.aiwg', 'sessions', 'host-catalog.sqlite');
|
|
25
|
+
}
|
|
26
|
+
/** Account-scoped adapters (Grok Bot today) route to the host catalog. */
|
|
27
|
+
export function isAccountScopedAdapter(adapter) {
|
|
28
|
+
return adapter.scopeClass === 'account';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether a discovery manifest holds any account-scoped source. Callers use
|
|
32
|
+
* this to avoid creating or opening the host catalog when every source is
|
|
33
|
+
* workspace-scoped. Unknown providers resolve to workspace scope.
|
|
34
|
+
*/
|
|
35
|
+
export function manifestNeedsHostCatalog(sources) {
|
|
36
|
+
if (sources.length === 0)
|
|
37
|
+
return false;
|
|
38
|
+
const registry = createSessionAdapterRegistry();
|
|
39
|
+
return sources.some((source) => {
|
|
40
|
+
try {
|
|
41
|
+
return isAccountScopedAdapter(registry.get(source.provider));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Aggregate identity: one account-scoped conversation counts once per provider. */
|
|
49
|
+
export function hostReferenceKey(identity) {
|
|
50
|
+
return `${identity.provider}\0${identity.nativeSessionId}`;
|
|
51
|
+
}
|
|
52
|
+
/** Drop cross-catalog duplicates, keeping the first row for each provider+session. */
|
|
53
|
+
export function dedupeSessionIdentities(rows) {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
return rows.filter((row) => {
|
|
56
|
+
const key = hostReferenceKey(row);
|
|
57
|
+
if (seen.has(key))
|
|
58
|
+
return false;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Count sessions across catalogs without doubling account-scoped replicas. */
|
|
64
|
+
export function countUniqueSessionsAcross(lists) {
|
|
65
|
+
return dedupeSessionIdentities(lists.flat()).length;
|
|
66
|
+
}
|
|
67
|
+
/** Host sessions backing a workspace catalog's references (matched by provider+session). */
|
|
68
|
+
export function resolveHostReferences(references, hostSessions) {
|
|
69
|
+
const byKey = new Map(hostSessions.map((session) => [
|
|
70
|
+
hostReferenceKey({ provider: session.provider, nativeSessionId: session.nativeSessionId }),
|
|
71
|
+
session,
|
|
72
|
+
]));
|
|
73
|
+
const resolved = [];
|
|
74
|
+
for (const reference of references) {
|
|
75
|
+
const session = byKey.get(hostReferenceKey(reference));
|
|
76
|
+
if (session)
|
|
77
|
+
resolved.push(session);
|
|
78
|
+
}
|
|
79
|
+
return resolved;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Workspace listing view: local rows plus referenced host sessions, with
|
|
83
|
+
* legacy duplicated copies (same provider+session already stored locally)
|
|
84
|
+
* counted once. Local rows win ties so workspace provenance is preserved.
|
|
85
|
+
*/
|
|
86
|
+
export function mergeSessionLists(local, referenced) {
|
|
87
|
+
return dedupeSessionIdentities([...local, ...referenced]);
|
|
88
|
+
}
|
|
89
|
+
/** Sessions in a repository reduced to aggregate identities (legacy rows included). */
|
|
90
|
+
export function collectSessionIdentities(repository, workspaceId) {
|
|
91
|
+
return repository.listSessionIdentities(workspaceId);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Count sessions across workspace and host catalogs without doubling
|
|
95
|
+
* account-scoped replicas: two workspaces referencing the same host
|
|
96
|
+
* session plus a legacy workspace holding a copied row count once.
|
|
97
|
+
*/
|
|
98
|
+
export function countUniqueSessionsAcrossCatalogs(views) {
|
|
99
|
+
return dedupeSessionIdentities(views.flatMap((view) => [...view.identities, ...view.references])).length;
|
|
100
|
+
}
|
|
101
|
+
/** Index visible identities by internal session id for merge lookups. */
|
|
102
|
+
export function indexIdentitiesBySessionId(identities) {
|
|
103
|
+
return new Map(identities.map((identity) => [identity.sessionId, identity]));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Merge local rows (search hits, analytics facts) with host-catalog rows,
|
|
107
|
+
* counting each session once by provider + native session id. Every local
|
|
108
|
+
* row is kept; a host row is dropped when the same session already has
|
|
109
|
+
* local rows (a legacy copied row wins over its host reference, mirroring
|
|
110
|
+
* mergeSessionLists, #3232) and host rows repeating across catalogs are
|
|
111
|
+
* deduped by hostRowKey. An unreachable host catalog contributes no rows,
|
|
112
|
+
* so local results are never lost.
|
|
113
|
+
*/
|
|
114
|
+
export function mergeCrossCatalogRows(localRows, hostRows, localIdentityBySessionId, hostIdentityBySessionId, hostRowKey) {
|
|
115
|
+
const localKeys = new Set();
|
|
116
|
+
for (const row of localRows) {
|
|
117
|
+
const identity = localIdentityBySessionId.get(row.sessionId);
|
|
118
|
+
if (identity) {
|
|
119
|
+
localKeys.add(hostReferenceKey({ provider: row.provider, nativeSessionId: identity.nativeSessionId }));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const seenHost = new Set();
|
|
123
|
+
const keptHost = hostRows.filter((row) => {
|
|
124
|
+
const identity = hostIdentityBySessionId.get(row.sessionId);
|
|
125
|
+
if (identity
|
|
126
|
+
&& localKeys.has(hostReferenceKey({ provider: row.provider, nativeSessionId: identity.nativeSessionId }))) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const key = hostRowKey(row);
|
|
130
|
+
if (seenHost.has(key))
|
|
131
|
+
return false;
|
|
132
|
+
seenHost.add(key);
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
return [...localRows, ...keptHost];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Publish staged account-scoped imports in the host catalog and record
|
|
139
|
+
* workspace references instead of copies (#3232). Non-destructive: legacy
|
|
140
|
+
* workspace rows are left untouched and stay readable.
|
|
141
|
+
*/
|
|
142
|
+
export function publishHostRoutedBatch(input) {
|
|
143
|
+
const accepted = new Set(input.sourceIds);
|
|
144
|
+
input.hostRepository.commitStagedBatch(input.batchRunId, [...accepted]);
|
|
145
|
+
for (const identity of input.hostRepository.listSessionIdentities(HOST_CATALOG_WORKSPACE_ID)) {
|
|
146
|
+
if (!accepted.has(identity.sourceId))
|
|
147
|
+
continue;
|
|
148
|
+
input.workspaceRepository.recordHostSessionReference({
|
|
149
|
+
provider: identity.provider,
|
|
150
|
+
nativeSessionId: identity.nativeSessionId,
|
|
151
|
+
sessionId: identity.sessionId,
|
|
152
|
+
sourceId: identity.sourceId,
|
|
153
|
+
hostCatalog: input.hostCatalogPath,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=host-catalog.js.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Content-addressed tool-output image references for the session catalog (#3265).
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
const IMAGE_DIGEST_PREFIX = 'sha256:';
|
|
4
|
+
export function sha256Bytes(value) {
|
|
5
|
+
return `${IMAGE_DIGEST_PREFIX}${createHash('sha256').update(value).digest('hex')}`;
|
|
6
|
+
}
|
|
7
|
+
export function isImageDigest(value) {
|
|
8
|
+
return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
9
|
+
}
|
|
10
|
+
/** Human-readable byte size for image placeholders (`3.1 MB`, `68 B`). */
|
|
11
|
+
export function formatImageBytes(byteLength) {
|
|
12
|
+
if (!Number.isFinite(byteLength) || byteLength < 0)
|
|
13
|
+
return '0 B';
|
|
14
|
+
if (byteLength < 1024)
|
|
15
|
+
return `${Math.trunc(byteLength)} B`;
|
|
16
|
+
const kb = byteLength / 1024;
|
|
17
|
+
if (kb < 1024)
|
|
18
|
+
return `${kb.toFixed(1)} KB`;
|
|
19
|
+
return `${(kb / 1024).toFixed(1)} MB`;
|
|
20
|
+
}
|
|
21
|
+
/** Placeholder kept in `searchableText` (and FTS) instead of the image bytes. */
|
|
22
|
+
export function imagePlaceholder(ref) {
|
|
23
|
+
return `[image: ${ref.digest}, ${ref.mediaType}, ${formatImageBytes(ref.byteLength)}]`;
|
|
24
|
+
}
|
|
25
|
+
// The payload class excludes whitespace so a match stops at the first word
|
|
26
|
+
// boundary instead of swallowing following prose; shape (and a minimum
|
|
27
|
+
// length well below any real image) is validated again on decode.
|
|
28
|
+
const DATA_URL_PATTERN = /data:(image\/[a-zA-Z0-9+.-]+)(;[a-zA-Z0-9!#$&^_.+-]+)*;base64,([A-Za-z0-9+/=]+)/g;
|
|
29
|
+
const MIN_BASE64_PAYLOAD_CHARS = 32;
|
|
30
|
+
function decodeBase64Payload(payload) {
|
|
31
|
+
const compact = payload.replace(/\s+/g, '');
|
|
32
|
+
// `Buffer.from(x, 'base64')` never throws -- it skips invalid characters --
|
|
33
|
+
// so validate the shape first: base64 alphabet only, padding at the end,
|
|
34
|
+
// and a length that can carry at least one byte.
|
|
35
|
+
if (compact.length < 4 || compact.length % 4 === 1)
|
|
36
|
+
return null;
|
|
37
|
+
if (!/^(?:[A-Za-z0-9+/]+={0,2})$/.test(compact))
|
|
38
|
+
return null;
|
|
39
|
+
const bytes = Buffer.from(compact, 'base64');
|
|
40
|
+
if (bytes.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return new Uint8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Replace inline base64 image data URLs in free text with content-hash
|
|
46
|
+
* placeholders. Only `image/*` media types are extracted; other data URLs
|
|
47
|
+
* (and remote `http(s)` image URLs, which are references already) pass
|
|
48
|
+
* through untouched.
|
|
49
|
+
*/
|
|
50
|
+
export function extractInlineImagesFromText(text, source = {}) {
|
|
51
|
+
const images = [];
|
|
52
|
+
if (!text.includes('data:image/'))
|
|
53
|
+
return { text, images };
|
|
54
|
+
DATA_URL_PATTERN.lastIndex = 0;
|
|
55
|
+
const result = text.replace(DATA_URL_PATTERN, (match, mediaType, _params, payload) => {
|
|
56
|
+
if (payload.length < MIN_BASE64_PAYLOAD_CHARS)
|
|
57
|
+
return match;
|
|
58
|
+
const bytes = decodeBase64Payload(payload);
|
|
59
|
+
if (!bytes)
|
|
60
|
+
return match;
|
|
61
|
+
const ref = {
|
|
62
|
+
digest: sha256Bytes(bytes),
|
|
63
|
+
mediaType: String(mediaType).toLowerCase(),
|
|
64
|
+
byteLength: bytes.length,
|
|
65
|
+
...(source.locatorClass !== undefined ? { locatorClass: source.locatorClass } : {}),
|
|
66
|
+
...(source.offset !== undefined ? { offset: source.offset } : {}),
|
|
67
|
+
bytes,
|
|
68
|
+
};
|
|
69
|
+
images.push(ref);
|
|
70
|
+
return imagePlaceholder(ref);
|
|
71
|
+
});
|
|
72
|
+
return { text: result, images };
|
|
73
|
+
}
|
|
74
|
+
function isPlainObject(value) {
|
|
75
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Extract inline images from a whole import batch. Returns replacement
|
|
79
|
+
* events (bytes swapped for references) plus the distinct blobs to persist.
|
|
80
|
+
* Idempotent: placeholder text from an earlier pass matches no pattern, so
|
|
81
|
+
* importer-extracted batches and direct writers converge on the same store
|
|
82
|
+
* state. Blobs travel beside the events because event JSON must only hold
|
|
83
|
+
* the content-hash reference, never the bytes.
|
|
84
|
+
*/
|
|
85
|
+
export function extractBatchEventImages(batch) {
|
|
86
|
+
const blobs = new Map();
|
|
87
|
+
for (const carried of batch.imageBlobs ?? []) {
|
|
88
|
+
if (isImageDigest(carried.digest) && !blobs.has(carried.digest))
|
|
89
|
+
blobs.set(carried.digest, carried);
|
|
90
|
+
}
|
|
91
|
+
const events = batch.events.map((event) => {
|
|
92
|
+
const extracted = extractEventImages({
|
|
93
|
+
searchableText: event.searchableText,
|
|
94
|
+
extensions: event.extensions,
|
|
95
|
+
rawReference: event.rawReference,
|
|
96
|
+
});
|
|
97
|
+
for (const image of extracted.images) {
|
|
98
|
+
if (!blobs.has(image.digest))
|
|
99
|
+
blobs.set(image.digest, image);
|
|
100
|
+
}
|
|
101
|
+
if (extracted.images.length === 0)
|
|
102
|
+
return event;
|
|
103
|
+
const refs = [...(event.imageRefs ?? [])];
|
|
104
|
+
for (const ref of extracted.imageRefs) {
|
|
105
|
+
if (!refs.some((prior) => prior.digest === ref.digest))
|
|
106
|
+
refs.push(ref);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...event,
|
|
110
|
+
searchableText: extracted.searchableText,
|
|
111
|
+
extensions: extracted.extensions,
|
|
112
|
+
imageRefs: refs,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return { events, imageBlobs: [...blobs.values()] };
|
|
116
|
+
}
|
|
117
|
+
/** Extract inline images from one event's text; extensions keep their existing redaction. */
|
|
118
|
+
export function extractEventImages(input) {
|
|
119
|
+
const source = {
|
|
120
|
+
...(input.rawReference?.locatorClass !== undefined ? { locatorClass: input.rawReference.locatorClass } : {}),
|
|
121
|
+
...(input.rawReference?.offset !== undefined ? { offset: input.rawReference.offset } : {}),
|
|
122
|
+
};
|
|
123
|
+
// Only event text is lifted. Provider image blocks in `extensions` are
|
|
124
|
+
// left to native-extension sanitization, which redacts them to markers,
|
|
125
|
+
// so this change never retains image bytes the catalog used to drop.
|
|
126
|
+
const text = extractInlineImagesFromText(input.searchableText ?? '', source);
|
|
127
|
+
const images = text.images;
|
|
128
|
+
return {
|
|
129
|
+
searchableText: text.text,
|
|
130
|
+
extensions: isPlainObject(input.extensions) ? input.extensions : {},
|
|
131
|
+
imageRefs: images.map((image) => ({
|
|
132
|
+
digest: image.digest,
|
|
133
|
+
mediaType: image.mediaType,
|
|
134
|
+
byteLength: image.byteLength,
|
|
135
|
+
...(image.locatorClass !== undefined ? { locatorClass: image.locatorClass } : {}),
|
|
136
|
+
...(image.offset !== undefined ? { offset: image.offset } : {}),
|
|
137
|
+
})),
|
|
138
|
+
images,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=image-blobs.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SESSION_CONTRACT_VERSION, SessionEventSchema, SessionSchema, assertSupportedSchemaMajor, sha256, stableEventId, stableSessionId, SessionContractError, schemaIssueDetail, assertSessionResourceLimit, } from './contracts.js';
|
|
2
2
|
import { assertImportByteLimit, DEFAULT_IMPORT_BYTE_LIMITS, resolveImportLimits } from './import-limits.js';
|
|
3
3
|
import { redactSessionValue, sanitizeSessionEvent, sanitizeNativeExtensions } from './policy.js';
|
|
4
|
+
import { extractEventImages } from './image-blobs.js';
|
|
4
5
|
import { fingerprintSourcePrefix } from './readers.js';
|
|
5
6
|
import { DEFAULT_INACTIVITY_THRESHOLD_MS } from './lifecycle.js';
|
|
6
7
|
import { classifySessionEventOrigin, deriveSessionIntent, } from './origin.js';
|
|
@@ -123,6 +124,7 @@ export class IncrementalSessionImporter {
|
|
|
123
124
|
source: sanitizedSource, run,
|
|
124
125
|
sessions: normalized.sessions, events: normalized.events,
|
|
125
126
|
batchRunId: request.batchRunId,
|
|
127
|
+
imageBlobs: normalized.imageBlobs,
|
|
126
128
|
}, { ...checkpoint }, request.publish ?? request.source.consistency !== 'complete');
|
|
127
129
|
const memory = process.memoryUsage();
|
|
128
130
|
peakHeap = Math.max(peakHeap, memory.heapUsed);
|
|
@@ -232,17 +234,30 @@ function recordIdentity(record) {
|
|
|
232
234
|
}
|
|
233
235
|
function normalizeBatch(request, records, digests, importRunId, sourceObservedAt) {
|
|
234
236
|
const sessions = new Map();
|
|
237
|
+
// Extract inline images before sanitization: provider image blocks in
|
|
238
|
+
// extensions are redacted to markers below, so bytes must be lifted out
|
|
239
|
+
// first while the raw record is still intact (#3265).
|
|
240
|
+
const imageBlobs = new Map();
|
|
235
241
|
const events = records.map((record, index) => {
|
|
236
242
|
const sessionId = stableSessionId(request.source.provider, request.source.sourceId, record.nativeSessionId);
|
|
237
243
|
const native = sanitizeNativeExtensions(record.extensions ?? {});
|
|
238
244
|
const lineage = request.source.provider === 'codex' && native.value.lineage
|
|
239
245
|
? { lineage: native.value.lineage } : {};
|
|
246
|
+
const images = extractEventImages({
|
|
247
|
+
searchableText: record.text,
|
|
248
|
+
extensions: record.extensions,
|
|
249
|
+
rawReference: record.rawReference,
|
|
250
|
+
});
|
|
251
|
+
for (const image of images.images) {
|
|
252
|
+
if (!imageBlobs.has(image.digest))
|
|
253
|
+
imageBlobs.set(image.digest, image);
|
|
254
|
+
}
|
|
240
255
|
const digest = digests[index];
|
|
241
256
|
const origin = classifySessionEventOrigin(request.source.provider, record);
|
|
242
257
|
const lifecycle = sessionLifecycle(request, record, sourceObservedAt);
|
|
243
258
|
const lifecycleEvidence = sessionLifecycleEvidence(request, record, sourceObservedAt);
|
|
244
259
|
const extensions = {
|
|
245
|
-
[`native.${request.source.provider}`]:
|
|
260
|
+
[`native.${request.source.provider}`]: images.extensions,
|
|
246
261
|
};
|
|
247
262
|
const event = sanitizeSessionEvent(SessionEventSchema.parse({
|
|
248
263
|
contractVersion: SESSION_CONTRACT_VERSION,
|
|
@@ -262,7 +277,8 @@ function normalizeBatch(request, records, digests, importRunId, sourceObservedAt
|
|
|
262
277
|
model: record.model ?? null,
|
|
263
278
|
entities: record.entities ?? [],
|
|
264
279
|
extractionState: record.extractionState ?? null,
|
|
265
|
-
searchableText:
|
|
280
|
+
searchableText: images.searchableText, imageRefs: images.imageRefs,
|
|
281
|
+
digest, rawReference: record.rawReference,
|
|
266
282
|
adapterVersion: request.adapter.adapterVersion,
|
|
267
283
|
consistency: request.source.consistency,
|
|
268
284
|
sensitivity: { classification: 'none', classes: [] },
|
|
@@ -309,7 +325,11 @@ function normalizeBatch(request, records, digests, importRunId, sourceObservedAt
|
|
|
309
325
|
for (const session of sessions.values()) {
|
|
310
326
|
session.intent = deriveSessionIntent(events.filter((event) => event.sessionId === session.sessionId));
|
|
311
327
|
}
|
|
312
|
-
return {
|
|
328
|
+
return {
|
|
329
|
+
sessions: redactSessionValue([...sessions.values()]).value,
|
|
330
|
+
events,
|
|
331
|
+
imageBlobs: [...imageBlobs.values()],
|
|
332
|
+
};
|
|
313
333
|
}
|
|
314
334
|
const KNOWN_EVENT_KINDS = new Set(['message', 'tool-call', 'tool-result', 'artifact', 'attachment', 'summary']);
|
|
315
335
|
function isKnownEventKind(kind) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './contracts.js';
|
|
2
|
+
export * from './image-blobs.js';
|
|
2
3
|
export * from './fixtures.js';
|
|
3
4
|
export * from './policy.js';
|
|
4
5
|
export * from './discovery.js';
|
|
@@ -19,6 +20,7 @@ export * from './importer.js';
|
|
|
19
20
|
export * from './import-lease.js';
|
|
20
21
|
export * from './batch-contracts.js';
|
|
21
22
|
export * from './batch-import.js';
|
|
23
|
+
export * from './host-catalog.js';
|
|
22
24
|
export * from './workspace-discovery.js';
|
|
23
25
|
export * from './timeline.js';
|
|
24
26
|
export * from './origin.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { requireFeaturePackage } from '../features/runtime.js';
|
|
4
4
|
import { readAiwgConfig } from '../config/aiwg-config.js';
|
|
@@ -6,6 +6,7 @@ import { createSessionAdapterRegistry } from './adapter-registry.js';
|
|
|
6
6
|
import { resolveImportExclusions } from './import-exclusions.js';
|
|
7
7
|
import { resolveImportLimits } from './import-limits.js';
|
|
8
8
|
import { acquireImportLease } from './import-lease.js';
|
|
9
|
+
import { defaultHostCatalogPath, manifestNeedsHostCatalog } from './host-catalog.js';
|
|
9
10
|
import { SessionRepository } from './repository.js';
|
|
10
11
|
import { importDiscoveryManifest } from './batch-import.js';
|
|
11
12
|
import { defaultDiscoveryManifestPath, discoverWorkspaceHistories, readDiscoveryManifest, writeDiscoveryManifest, } from './workspace-discovery.js';
|
|
@@ -69,8 +70,26 @@ export async function refreshSessionCatalog(workspace, full, waitMs, signal, wor
|
|
|
69
70
|
return 'no authorized provider roots';
|
|
70
71
|
}
|
|
71
72
|
const lease = await acquireImportLease(database, `index-refresh-${process.pid}`, { waitMs });
|
|
73
|
+
let hostLease;
|
|
72
74
|
try {
|
|
73
75
|
const repository = new SessionRepository(database);
|
|
76
|
+
// Account-scoped providers import once into the host catalog; the
|
|
77
|
+
// hourly host refresh (#310) routes them there instead of copying
|
|
78
|
+
// them into every workspace catalog (#3232). The host catalog is only
|
|
79
|
+
// opened when a manifest actually holds an account-scoped source.
|
|
80
|
+
const hostCatalogPath = defaultHostCatalogPath();
|
|
81
|
+
const needsHost = manifests.some((entry) => manifestNeedsHostCatalog(entry.manifest.sources));
|
|
82
|
+
let hostRepository;
|
|
83
|
+
if (needsHost) {
|
|
84
|
+
mkdirSync(path.dirname(hostCatalogPath), { recursive: true, mode: 0o700 });
|
|
85
|
+
// The workspace lease above does not cover the shared host catalog,
|
|
86
|
+
// so overlapping refreshes from two workspaces would stage the same
|
|
87
|
+
// replicas concurrently (#3232). Serialize host writers with the
|
|
88
|
+
// same lock pattern; the shared checkpoint then turns the second
|
|
89
|
+
// writer into duplicate references instead of a double import.
|
|
90
|
+
hostLease = await acquireImportLease(hostCatalogPath, `host-refresh-${process.pid}`, { waitMs });
|
|
91
|
+
hostRepository = new SessionRepository(hostCatalogPath);
|
|
92
|
+
}
|
|
74
93
|
try {
|
|
75
94
|
if (full)
|
|
76
95
|
repository.reindex();
|
|
@@ -82,7 +101,9 @@ export async function refreshSessionCatalog(workspace, full, waitMs, signal, wor
|
|
|
82
101
|
continue;
|
|
83
102
|
// Persist the exact discovered batch before its confirmed import.
|
|
84
103
|
await writeDiscoveryManifest(entry.path, entry.manifest);
|
|
85
|
-
const receipt = await importDiscoveryManifest({
|
|
104
|
+
const receipt = await importDiscoveryManifest({
|
|
105
|
+
manifest: entry.manifest, repository, hostRepository, hostCatalogPath, signal, limits, exclude,
|
|
106
|
+
});
|
|
86
107
|
if (receipt.totals.pending) {
|
|
87
108
|
throw new Error(`session import incomplete: ${receipt.totals.rejected} rejected, ${receipt.totals.pending} pending`);
|
|
88
109
|
}
|
|
@@ -94,10 +115,12 @@ export async function refreshSessionCatalog(workspace, full, waitMs, signal, wor
|
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
finally {
|
|
118
|
+
hostRepository?.close();
|
|
97
119
|
repository.close();
|
|
98
120
|
}
|
|
99
121
|
}
|
|
100
122
|
finally {
|
|
123
|
+
await hostLease?.release();
|
|
101
124
|
await lease.release();
|
|
102
125
|
}
|
|
103
126
|
return undefined;
|