@aiwg/cli 2026.10.10 → 2026.10.12
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/agentic/code/providers/model-catalog.v1.json +62 -12
- package/dist/src/artifacts/binding-target-identity.js +51 -0
- package/dist/src/artifacts/checksum-manifest.js +12 -0
- package/dist/src/artifacts/index-builder.js +11 -13
- package/dist/src/artifacts/query-engine.js +5 -11
- package/dist/src/cli/handlers/doctor-help.js +32 -0
- package/dist/src/cli/handlers/gates.js +19 -18
- package/dist/src/cli/handlers/models-help.js +7 -1
- package/dist/src/cli/handlers/models.js +3 -3
- package/dist/src/cli/handlers/script-runner.js +8 -5
- package/dist/src/cli/handlers/sessions-intake.js +104 -0
- package/dist/src/cli/handlers/sessions-tail.js +172 -0
- package/dist/src/cli/handlers/sessions.js +287 -31
- package/dist/src/cli/handlers/skill-lint.js +10 -3
- package/dist/src/cli/handlers/steward-format.js +100 -0
- package/dist/src/cli/handlers/steward-models.js +167 -0
- package/dist/src/cli/handlers/steward.js +14 -248
- package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
- package/dist/src/cli/handlers/use.js +18 -28
- package/dist/src/cli/handlers/utilities.js +1 -24
- package/dist/src/config/aiwg-config.js +7 -0
- package/dist/src/config/authorization-cli.js +89 -0
- package/dist/src/config/cli-help.js +1 -0
- package/dist/src/config/cli.js +10 -1
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/decision/context-pruning.js +3 -2
- package/dist/src/decision/ensemble/contract.js +15 -4
- package/dist/src/decision/ensemble-study/artifacts.js +5 -0
- package/dist/src/decision/ensemble-study/score.js +4 -3
- package/dist/src/decision/feature-export.js +3 -9
- package/dist/src/decision/heldout/collector.js +2 -2
- package/dist/src/decision/issue-triage/runtime.js +3 -9
- package/dist/src/decision/qualification/gates.js +2 -1
- package/dist/src/decision/qualification/release.js +2 -3
- package/dist/src/decision/routing/report.js +2 -0
- package/dist/src/decision/sdlc-screening.js +3 -5
- package/dist/src/decision/sensitivity/comparative.js +5 -3
- package/dist/src/gates/driver.js +6 -77
- package/dist/src/gates/evaluate.js +4 -11
- package/dist/src/gates/floors.js +4 -4
- package/dist/src/gates/index.js +3 -3
- package/dist/src/gates/lattice.js +26 -0
- package/dist/src/gates/registry.js +2 -1
- package/dist/src/gates/report.js +19 -7
- package/dist/src/issues/audit-cli.js +120 -0
- package/dist/src/issues/audit-contract.js +64 -0
- package/dist/src/issues/audit-local.js +82 -0
- package/dist/src/issues/audit.js +220 -0
- package/dist/src/lint/rule-activation.js +15 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
- package/dist/src/mission-protocol/canonical-codecs.js +270 -0
- package/dist/src/mission-protocol/codecs.js +22 -550
- package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
- package/dist/src/mission-protocol/health-trace-types.js +43 -0
- package/dist/src/mission-protocol/types.js +2 -42
- package/dist/src/mission-protocol/worker-contract-json.js +10 -0
- package/dist/src/mission-protocol/worker-review-gate.js +100 -0
- package/dist/src/mission-protocol/worker-review-types.js +2 -0
- package/dist/src/models/catalog-policy.js +280 -0
- package/dist/src/models/index.js +1 -1
- package/dist/src/models/model-capabilities.v1.json +78 -14
- package/dist/src/models/model-catalog.v1.json +62 -12
- package/dist/src/models/policy-resources.js +28 -0
- package/dist/src/models/provider-policy.js +10 -539
- package/dist/src/models/resolver.js +26 -8
- package/dist/src/models/router.js +15 -2
- package/dist/src/models/tiers.js +13 -0
- package/dist/src/models/types.js +0 -6
- package/dist/src/models/worker-ceilings.js +59 -0
- package/dist/src/models/worker-invocation-policy.js +243 -0
- package/dist/src/models/worker-plan.js +16 -20
- package/dist/src/models/worker-review-lineage.js +55 -0
- package/dist/src/models/worker-selection-policy.js +1 -1
- package/dist/src/models/wrapper-route.js +6 -3
- package/dist/src/policy/authorization.js +30 -1
- package/dist/src/policy/hitl-config.js +63 -0
- package/dist/src/policy/hitl-receipt.js +57 -0
- package/dist/src/policy/hitl-types.js +4 -0
- package/dist/src/policy/hitl-validation.js +39 -0
- package/dist/src/policy/hitl.js +117 -0
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/rules/activation.d.mts +1 -1
- package/dist/src/rules/activation.mjs +0 -2
- package/dist/src/rules/gate-coverage.js +51 -0
- package/dist/src/rules/gate-references.js +16 -0
- package/dist/src/rules/rule-frontmatter.schema.json +7 -1
- package/dist/src/serve/worker-command-request.js +21 -4
- package/dist/src/serve/worker-invocation-preparation.js +3 -1
- package/dist/src/serve/worker-invocation.js +21 -10
- package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
- package/dist/src/sessions/adapters/grok-build-native.js +269 -0
- package/dist/src/sessions/adapters/grok-build.js +57 -15
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
- package/dist/src/sessions/adapters/hermes.js +42 -3
- package/dist/src/sessions/adapters/warp.js +1 -1
- package/dist/src/sessions/batch-import.js +44 -6
- package/dist/src/sessions/contracts.js +20 -1
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/discovery-capability.js +38 -0
- 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 +9 -232
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/intake-dir.js +71 -0
- package/dist/src/sessions/normalization.js +251 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/tail.js +91 -0
- package/dist/src/sessions/workspace-discovery.js +88 -9
- package/dist/src/skills/agent-skill-sidecar.js +28 -0
- package/dist/src/skills/agent-skills.js +4 -23
- package/dist/src/skills/claude-surface-projection.js +25 -0
- package/dist/src/skills/deployer.js +8 -288
- package/dist/src/skills/deployment-policy.js +114 -0
- package/dist/src/skills/deployment-projection.js +152 -0
- package/dist/src/skills/deployment-types.js +20 -0
- package/dist/src/skills/exporter.js +8 -31
- package/dist/src/skills/provider-extensions.js +41 -0
- package/dist/src/skills/run.js +6 -0
- package/dist/src/skills/types.js +0 -8
- package/dist/src/skills/validator-resources.js +68 -0
- package/dist/src/skills/validator.js +6 -55
- package/dist/src/storage/cli-migrate-options.js +125 -0
- package/dist/src/storage/cli-migrate.js +111 -0
- package/dist/src/storage/cli.js +4 -279
- package/dist/src/storage/migration-adapter.js +79 -0
- package/dist/src/storage/migration-copy.js +108 -0
- package/dist/src/storage/migration-protocol.js +14 -197
- package/dist/src/storage/migration-records.js +186 -0
- package/dist/src/storage/migration-types.js +12 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/tracker/issue-pages.js +110 -0
- package/package.json +7 -1
- package/schemas/mission-v1.schema.json +335 -0
- package/schemas/models/model-catalog.v1.schema.json +19 -1
- package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
- package/schemas/models/model-policy.v1.schema.json +1 -1
- package/tools/agents/providers/provider-paths-staging.mjs +35 -0
- package/tools/agents/providers/rule-binding-targets.mjs +146 -0
- package/tools/agents/providers/rule-delivery.mjs +7 -126
- 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
- package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { z } from 'zod';
|
|
2
4
|
import { number } from '../../schema/zod-validation.mjs';
|
|
3
5
|
import { schemaRejectionDetail, schemaIssueDetail, SessionContractError, assertSupportedSchemaMajor, } from '../contracts.js';
|
|
4
6
|
import { streamBoundedJsonLines, } from '../readers.js';
|
|
7
|
+
import { HERMES_SNAPSHOT_LOCATOR_CLASS, HERMES_STATE_DB_FILENAME, hermesSnapshotStagingDir, snapshotHermesStateDb, } from './hermes-state-db.js';
|
|
5
8
|
export const HERMES_ADAPTER_VERSION = '1.0.0';
|
|
6
9
|
export const HERMES_EXPORT_SCHEMA_VERSION = '1.0.0';
|
|
7
10
|
export const HERMES_NATIVE_SCHEMA_VERSION = '23.0.0';
|
|
@@ -64,14 +67,42 @@ export class HermesSessionAdapter {
|
|
|
64
67
|
provider = 'hermes';
|
|
65
68
|
adapterVersion = HERMES_ADAPTER_VERSION;
|
|
66
69
|
disposition = 'implemented';
|
|
67
|
-
supportedOperations = ['inspect', 'stream'];
|
|
70
|
+
supportedOperations = ['discover', 'inspect', 'stream'];
|
|
68
71
|
acquisitionModes = ['jsonl', 'api', 'sqlite-snapshot'];
|
|
69
72
|
constructor(limits, localApi) {
|
|
70
73
|
this.limits = limits;
|
|
71
74
|
this.localApi = localApi;
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Discover through a consistent `sqlite3.backup()` snapshot of the
|
|
78
|
+
* explicitly authorized `<root>/state.db`. The live database is never
|
|
79
|
+
* opened for writing and never streamed directly; matching sessions are
|
|
80
|
+
* projected to an owned staging snapshot the existing consistent-snapshot
|
|
81
|
+
* path already validates.
|
|
82
|
+
*/
|
|
83
|
+
async *discover(scope) {
|
|
84
|
+
if (scope.allowedRoots.length === 0) {
|
|
85
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'Hermes discovery requires an explicitly authorized Hermes home; pass --hermes-root.');
|
|
86
|
+
}
|
|
87
|
+
const staging = hermesSnapshotStagingDir(scope.workspaceId);
|
|
88
|
+
for (const root of [...scope.allowedRoots].sort()) {
|
|
89
|
+
if (!(await isStateDbFile(join(root, HERMES_STATE_DB_FILENAME))))
|
|
90
|
+
continue;
|
|
91
|
+
const snapshot = await snapshotHermesStateDb({
|
|
92
|
+
hermesRoot: root,
|
|
93
|
+
stagingDir: staging,
|
|
94
|
+
allowedRoots: scope.allowedRoots,
|
|
95
|
+
workspaceId: scope.workspaceId,
|
|
96
|
+
limits: this.limits,
|
|
97
|
+
});
|
|
98
|
+
if (!snapshot)
|
|
99
|
+
continue;
|
|
100
|
+
yield {
|
|
101
|
+
provider: 'hermes',
|
|
102
|
+
locator: snapshot.projectionPath,
|
|
103
|
+
locatorClass: HERMES_SNAPSHOT_LOCATOR_CLASS,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
75
106
|
}
|
|
76
107
|
async inspect(source) {
|
|
77
108
|
if (source.locatorClass === 'hermes-export-jsonl'
|
|
@@ -284,6 +315,14 @@ function normalizeMessage(sessionId, message, index, line, locatorClass, common)
|
|
|
284
315
|
}));
|
|
285
316
|
return records;
|
|
286
317
|
}
|
|
318
|
+
async function isStateDbFile(path) {
|
|
319
|
+
try {
|
|
320
|
+
return (await lstat(path)).isFile();
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
287
326
|
function ended(value) {
|
|
288
327
|
return Boolean(value.ended_at || value.provider_deleted_at || value.archived || value.inactive);
|
|
289
328
|
}
|
|
@@ -50,7 +50,7 @@ export class WarpSessionAdapter {
|
|
|
50
50
|
if (blocks.length === 0) {
|
|
51
51
|
throw new SessionContractError('MALFORMED_SOURCE', 'Warp Markdown export contains no recognized user or assistant conversation blocks', schemaIssueDetail('headings'));
|
|
52
52
|
}
|
|
53
|
-
const stableSessionId = `manual:${createHash('sha256').update(value).digest('hex').slice(0, 32)}`;
|
|
53
|
+
const stableSessionId = source.nativeSessionId ?? `manual:${createHash('sha256').update(value).digest('hex').slice(0, 32)}`;
|
|
54
54
|
const lifecycleEvidence = /<!--\s*warp-lifecycle:\s*complete\s*-->/i.test(value)
|
|
55
55
|
? 'completed-at-import' : 'unknown-at-import';
|
|
56
56
|
const title = /^#\s+(.+)$/m.exec(value)?.[1]?.trim();
|
|
@@ -6,6 +6,7 @@ import { coveragePreflight, sessionRuntimeIdentity } from './coverage-preflight.
|
|
|
6
6
|
import { SESSION_CONTRACT_VERSION, SessionContractError, schemaIssueDetail, SessionSourceSchema, sha256, stableSessionId, stableEventId, } from './contracts.js';
|
|
7
7
|
import { BATCH_IMPORT_VERSION, coverageFromBatchRun, } from './batch-contracts.js';
|
|
8
8
|
import { IncrementalSessionImporter, SessionImportFailure } from './importer.js';
|
|
9
|
+
import { HOST_CATALOG_WORKSPACE_ID, isAccountScopedAdapter, publishHostRoutedBatch, } from './host-catalog.js';
|
|
9
10
|
import { matchImportExclusion, resolveImportExclusions } from './import-exclusions.js';
|
|
10
11
|
import { resolveImportLimits } from './import-limits.js';
|
|
11
12
|
import { fingerprintSourceFile } from './readers.js';
|
|
@@ -45,6 +46,7 @@ export async function importDiscoveryManifest(options) {
|
|
|
45
46
|
run.completedAt = null;
|
|
46
47
|
options.repository.saveBatchImportRun(run);
|
|
47
48
|
injectFault(options, run, 'run-saved');
|
|
49
|
+
const hostRoutedSourceIds = [];
|
|
48
50
|
for (const manifestSource of options.manifest.sources) {
|
|
49
51
|
const disposition = run.sources.find((candidate) => candidate.sourceId === manifestSource.sourceId);
|
|
50
52
|
if (!disposition) {
|
|
@@ -76,23 +78,33 @@ export async function importDiscoveryManifest(options) {
|
|
|
76
78
|
persistProgress(options.repository, run, now());
|
|
77
79
|
try {
|
|
78
80
|
await assertManifestSourceUnchanged(manifestSource);
|
|
81
|
+
const adapter = adapterFor(manifestSource.provider, limits);
|
|
82
|
+
// Account-scoped providers (Grok Bot) are imported once into the
|
|
83
|
+
// host catalog; the workspace catalog records a reference (#3232).
|
|
84
|
+
// Any adapter declaring account scope follows this route.
|
|
85
|
+
const hostRouted = options.hostRepository !== undefined
|
|
86
|
+
&& options.hostCatalogPath !== undefined
|
|
87
|
+
&& isAccountScopedAdapter(adapter);
|
|
88
|
+
const targetRepository = hostRouted ? options.hostRepository : options.repository;
|
|
89
|
+
const importWorkspaceId = hostRouted ? HOST_CATALOG_WORKSPACE_ID : options.manifest.workspaceId;
|
|
90
|
+
if (hostRouted)
|
|
91
|
+
hostRoutedSourceIds.push(manifestSource.sourceId);
|
|
79
92
|
const selectedSource = {
|
|
80
93
|
provider: manifestSource.provider,
|
|
81
94
|
locator: manifestSource.locator,
|
|
82
95
|
locatorClass: manifestSource.locatorClass,
|
|
83
96
|
sourceId: manifestSource.sourceId,
|
|
84
97
|
authorizedScope: {
|
|
85
|
-
workspaceId:
|
|
98
|
+
workspaceId: importWorkspaceId,
|
|
86
99
|
allowedRoots: [manifestSource.authorizedRoot],
|
|
87
100
|
},
|
|
88
101
|
};
|
|
89
|
-
const adapter = adapterFor(manifestSource.provider, limits);
|
|
90
102
|
const source = await sessionSource(selectedSource, adapter);
|
|
91
|
-
const receipts = await new IncrementalSessionImporter(
|
|
103
|
+
const receipts = await new IncrementalSessionImporter(targetRepository).import({
|
|
92
104
|
source,
|
|
93
105
|
selectedSource,
|
|
94
106
|
adapter,
|
|
95
|
-
workspaceId:
|
|
107
|
+
workspaceId: importWorkspaceId,
|
|
96
108
|
policyVersion: '1.0.0',
|
|
97
109
|
batchRunId: run.runId,
|
|
98
110
|
publish: false,
|
|
@@ -101,7 +113,7 @@ export async function importDiscoveryManifest(options) {
|
|
|
101
113
|
limits,
|
|
102
114
|
});
|
|
103
115
|
await assertManifestSourceUnchanged(manifestSource);
|
|
104
|
-
const totals =
|
|
116
|
+
const totals = targetRepository.sourceImportTotals(manifestSource.sourceId, run.runId);
|
|
105
117
|
disposition.sessionsAccepted = totals.sessions;
|
|
106
118
|
disposition.eventsAccepted = totals.events;
|
|
107
119
|
disposition.status = receipts.length > 0
|
|
@@ -134,6 +146,18 @@ export async function importDiscoveryManifest(options) {
|
|
|
134
146
|
.map((source) => source.sourceId);
|
|
135
147
|
injectFault(options, run, 'before-publication');
|
|
136
148
|
options.repository.commitStagedBatch(run.runId, publishable);
|
|
149
|
+
if (options.hostRepository && options.hostCatalogPath && hostRoutedSourceIds.length > 0) {
|
|
150
|
+
const acceptedHost = run.sources
|
|
151
|
+
.filter((source) => hostRoutedSourceIds.includes(source.sourceId) && isAccepted(source.status))
|
|
152
|
+
.map((source) => source.sourceId);
|
|
153
|
+
publishHostRoutedBatch({
|
|
154
|
+
hostRepository: options.hostRepository,
|
|
155
|
+
workspaceRepository: options.repository,
|
|
156
|
+
batchRunId: run.runId,
|
|
157
|
+
sourceIds: acceptedHost,
|
|
158
|
+
hostCatalogPath: options.hostCatalogPath,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
137
161
|
injectFault(options, run, 'after-publication');
|
|
138
162
|
run.status = pending ? 'interrupted' : rejected || acquisitionIncomplete ? 'partial' : 'complete';
|
|
139
163
|
run.updatedAt = now().toISOString();
|
|
@@ -303,6 +327,16 @@ function providerProfile(provider, locatorClass) {
|
|
|
303
327
|
return 'muse-native-session-log-v1';
|
|
304
328
|
if (provider === 'grokbot')
|
|
305
329
|
return 'grokbot-native-transcript-replica-v1';
|
|
330
|
+
if (provider === 'grok-build') {
|
|
331
|
+
return locatorClass === 'grok-build-native-chat-history'
|
|
332
|
+
? 'grok-build-native-chat-history-v1'
|
|
333
|
+
: 'documented-cli-markdown-export';
|
|
334
|
+
}
|
|
335
|
+
if (provider === 'hermes') {
|
|
336
|
+
return locatorClass === 'hermes-consistent-snapshot-jsonl'
|
|
337
|
+
? 'native-state-db-backup-snapshot-v1'
|
|
338
|
+
: 'native-schema-23-export';
|
|
339
|
+
}
|
|
306
340
|
return 'manual-interchange';
|
|
307
341
|
}
|
|
308
342
|
function importErrorCode(error) {
|
|
@@ -360,8 +394,12 @@ async function rebuildFromCatalog(options) {
|
|
|
360
394
|
let staged;
|
|
361
395
|
try {
|
|
362
396
|
staged = new SessionRepository(join(stagingDirectory, 'staging.sqlite'));
|
|
397
|
+
// The staged rebuild transfers normalized rows (not host references) into
|
|
398
|
+
// the destination, so account-scoped sources rebuild locally here as
|
|
399
|
+
// before; the destination's own host routing applies on its next import.
|
|
363
400
|
const receipt = await importDiscoveryManifest({ ...options, manifest, repository: staged,
|
|
364
|
-
carryForward: undefined, fault: undefined
|
|
401
|
+
carryForward: undefined, fault: undefined,
|
|
402
|
+
hostRepository: undefined, hostCatalogPath: undefined });
|
|
365
403
|
const identities = new Set();
|
|
366
404
|
const destinationCodex = new Map(catalogSessions(options.repository, workspaceId)
|
|
367
405
|
.filter(session => session.provider === 'codex').map(session => [session.nativeSessionId, session.sourceId]));
|
|
@@ -76,7 +76,7 @@ export const SessionErrorCodeSchema = z.enum([
|
|
|
76
76
|
'OPERATION_NOT_AUTHORIZED',
|
|
77
77
|
'IMPORT_CONFLICT', 'IMPORT_INTERRUPTED', 'MALFORMED_SOURCE',
|
|
78
78
|
'DUPLICATE_NATIVE_ID', 'AMBIGUOUS_TIMESTAMP', 'TRUNCATED_SOURCE',
|
|
79
|
-
'UNSUPPORTED_OPERATION',
|
|
79
|
+
'UNSUPPORTED_OPERATION', 'UNSUPPORTED_PROVIDER',
|
|
80
80
|
'INVALID_SEARCH_QUERY', 'INVALID_ARGUMENT',
|
|
81
81
|
]);
|
|
82
82
|
const VersionSchema = z.string().regex(/^\d+\.\d+\.\d+(?:[-+].+)?$/);
|
|
@@ -103,6 +103,19 @@ export const SessionSourceSchema = z.object({
|
|
|
103
103
|
authorizedAt: dateTime({ offset: true }),
|
|
104
104
|
extensions: NativeExtensionsSchema.prefault({}),
|
|
105
105
|
});
|
|
106
|
+
/**
|
|
107
|
+
* Content-addressed reference to a tool-output image whose bytes live in
|
|
108
|
+
* the catalog blob store instead of inline in the event (#3265). The
|
|
109
|
+
* persisted shape's schema is this zod contract (session events have no
|
|
110
|
+
* JSON-schema file; see `lint:schemas`).
|
|
111
|
+
*/
|
|
112
|
+
export const SessionImageRefSchema = z.object({
|
|
113
|
+
digest: DigestSchema,
|
|
114
|
+
mediaType: z.string().min(1),
|
|
115
|
+
byteLength: integer().nonnegative(),
|
|
116
|
+
locatorClass: z.string().min(1).optional(),
|
|
117
|
+
offset: integer().nonnegative().optional(),
|
|
118
|
+
});
|
|
106
119
|
export const SessionEventSchema = z.object({
|
|
107
120
|
contractVersion: z.literal(SESSION_CONTRACT_VERSION),
|
|
108
121
|
eventId: z.string().min(1),
|
|
@@ -130,6 +143,12 @@ export const SessionEventSchema = z.object({
|
|
|
130
143
|
originRule: z.string().min(1).default('legacy:unknown'),
|
|
131
144
|
originClassifierVersion: VersionSchema.default('1.0.0'),
|
|
132
145
|
searchableText: z.string(),
|
|
146
|
+
/**
|
|
147
|
+
* Image references extracted from inline payloads at import. Optional so
|
|
148
|
+
* pre-#3265 catalog rows (inline bytes, field absent) still parse; readers
|
|
149
|
+
* must tolerate `undefined` as "no references".
|
|
150
|
+
*/
|
|
151
|
+
imageRefs: z.array(SessionImageRefSchema).optional(),
|
|
133
152
|
digest: DigestSchema,
|
|
134
153
|
rawReference: z.object({
|
|
135
154
|
locatorClass: z.string().min(1),
|
|
@@ -139,7 +139,13 @@ export function reconcileCoveragePreflight(preflight, sources) {
|
|
|
139
139
|
return entry;
|
|
140
140
|
const selected = sources.filter((source) => source.provider === entry.provider);
|
|
141
141
|
const imported = selected.length === entry.sourceCount && selected.length > 0
|
|
142
|
-
|
|
142
|
+
// duplicate/previously-committed sources were acquired by an earlier
|
|
143
|
+
// run, so zero new events still counts as imported. A host-routed
|
|
144
|
+
// second workspace import lands here: the host copy already exists
|
|
145
|
+
// and the workspace only records references (#3232). A freshly
|
|
146
|
+
// committed source with zero events stays incomplete.
|
|
147
|
+
&& selected.every((source) => source.status === 'excluded' || (accepted.has(source.status)
|
|
148
|
+
&& (source.eventsAccepted > 0 || source.status !== 'committed')));
|
|
143
149
|
return {
|
|
144
150
|
...entry,
|
|
145
151
|
disposition: !imported ? 'import-incomplete'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const REQUIRED_FLAGS = {
|
|
2
|
+
claude: [],
|
|
3
|
+
codex: ['--codex-root'],
|
|
4
|
+
cursor: [],
|
|
5
|
+
factory: [],
|
|
6
|
+
pi: [],
|
|
7
|
+
omp: ['--omp-root'],
|
|
8
|
+
'deepseek-harness': ['--dsh-root'],
|
|
9
|
+
muse: ['--muse-root'],
|
|
10
|
+
grokbot: ['--grokbot-root'],
|
|
11
|
+
'grok-build': ['--grok-root'],
|
|
12
|
+
hermes: ['--hermes-root'],
|
|
13
|
+
};
|
|
14
|
+
const LIVE_DETECTION = {
|
|
15
|
+
'grok-build': 'supported',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* `discoverable` requires a wired `discover` operation on the loaded adapter,
|
|
19
|
+
* not merely an implemented ingest path; otherwise the provider stays
|
|
20
|
+
* `manual-export` (or `unsupported` when it has no ingest path at all).
|
|
21
|
+
*/
|
|
22
|
+
export function providerDiscoveryCapability(provider, supportedOperations) {
|
|
23
|
+
if (supportedOperations.includes('discover')) {
|
|
24
|
+
return {
|
|
25
|
+
disposition: 'discoverable',
|
|
26
|
+
authorization: 'explicit-root',
|
|
27
|
+
requiredFlags: REQUIRED_FLAGS[provider] ?? [],
|
|
28
|
+
liveDetection: LIVE_DETECTION[provider] ?? 'unsupported',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
disposition: 'manual-export',
|
|
33
|
+
authorization: 'none',
|
|
34
|
+
requiredFlags: [],
|
|
35
|
+
liveDetection: 'unsupported',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=discovery-capability.js.map
|
|
@@ -75,7 +75,16 @@ export function sessionToAiwgFortemiRecord(session, catalogTags = []) {
|
|
|
75
75
|
updated_at: updatedAt,
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Map one catalog event. `images` carries the event's image references
|
|
80
|
+
* resolved against the blob store when the caller can provide them
|
|
81
|
+
* (export build passes `repository.resolveEventImages`); omitted means
|
|
82
|
+
* "not resolved", never "no images" -- the placeholder text in `text` is
|
|
83
|
+
* always present regardless (#3265). Bytes are never embedded here, only
|
|
84
|
+
* the reference plus its availability, so a consumer knows whether the
|
|
85
|
+
* digest is fetchable from the catalog or gone.
|
|
86
|
+
*/
|
|
87
|
+
export function sessionEventToAiwgFortemiRecord(session, event, images = []) {
|
|
79
88
|
const locator = sessionEventRecordLocator(session, event);
|
|
80
89
|
const parentLocator = sessionRecordLocator(session);
|
|
81
90
|
const updatedAt = event.occurredAt ?? new Date(0).toISOString();
|
|
@@ -110,6 +119,16 @@ export function sessionEventToAiwgFortemiRecord(session, event) {
|
|
|
110
119
|
nativeId: event.nativeId,
|
|
111
120
|
sequence: event.sequence,
|
|
112
121
|
toolCallId: event.toolCallId,
|
|
122
|
+
...(images.length > 0
|
|
123
|
+
? {
|
|
124
|
+
images: images.map((image) => ({
|
|
125
|
+
digest: image.digest,
|
|
126
|
+
mediaType: image.mediaType,
|
|
127
|
+
byteLength: image.byteLength,
|
|
128
|
+
status: image.status,
|
|
129
|
+
})),
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
113
132
|
},
|
|
114
133
|
}];
|
|
115
134
|
const tags = [
|
|
@@ -117,6 +136,10 @@ export function sessionEventToAiwgFortemiRecord(session, event) {
|
|
|
117
136
|
...(event.role ? [tag('role', event.role)] : []),
|
|
118
137
|
...(event.toolName ? [tag('tool', event.toolName)] : []),
|
|
119
138
|
...(event.entities ?? []).map((entity) => tag('entity', entity)),
|
|
139
|
+
...images.map((image) => tag('image', image.digest)),
|
|
140
|
+
...images
|
|
141
|
+
.filter((image) => image.status === 'unavailable')
|
|
142
|
+
.map((image) => tag('image-unavailable', image.digest)),
|
|
120
143
|
];
|
|
121
144
|
return {
|
|
122
145
|
schema_version: SESSION_EXPORT_RECORD_SCHEMA_VERSION,
|
|
@@ -275,13 +298,20 @@ function exportManifestRecord(repoRef, generatedAt) {
|
|
|
275
298
|
* session catalog's workspace id, not a git repo -- the field is repurposed
|
|
276
299
|
* honestly rather than left to imply something untrue).
|
|
277
300
|
*/
|
|
278
|
-
export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = [], catalogTagsBySessionId = new Map()
|
|
301
|
+
export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = [], catalogTagsBySessionId = new Map(),
|
|
302
|
+
/**
|
|
303
|
+
* Resolve image references per event when the caller's catalog is at
|
|
304
|
+
* hand (`(event) => repository.resolveEventImages(event)`). Absent means
|
|
305
|
+
* references stay unresolved in the records, never that images are
|
|
306
|
+
* missing -- availability is only asserted when actually resolved (#3265).
|
|
307
|
+
*/
|
|
308
|
+
imageResolution) {
|
|
279
309
|
const generatedAt = new Date().toISOString();
|
|
280
310
|
const items = [exportManifestRecord(repoRef, generatedAt)];
|
|
281
311
|
for (const session of sessions) {
|
|
282
312
|
items.push(sessionToAiwgFortemiRecord(session, catalogTagsBySessionId.get(session.sessionId) ?? []));
|
|
283
313
|
for (const event of eventsBySessionId.get(session.sessionId) ?? []) {
|
|
284
|
-
items.push(sessionEventToAiwgFortemiRecord(session, event));
|
|
314
|
+
items.push(sessionEventToAiwgFortemiRecord(session, event, imageResolution?.(event) ?? []));
|
|
285
315
|
}
|
|
286
316
|
for (const output of outputs.filter((entry) => entry.sessionId === session.sessionId)) {
|
|
287
317
|
items.push(sessionOutputToAiwgFortemiRecord(session, output));
|
|
@@ -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
|