@kaelio/ktx 0.6.0 → 0.8.0
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/assets/python/{kaelio_ktx-0.6.0-py3-none-any.whl → kaelio_ktx-0.8.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli-program.js +7 -0
- package/dist/command-schemas.d.ts +1 -1
- package/dist/command-tree.js +5 -1
- package/dist/commands/completion-commands.d.ts +3 -0
- package/dist/commands/completion-commands.js +38 -0
- package/dist/commands/ingest-commands.js +0 -4
- package/dist/commands/knowledge-commands.js +15 -2
- package/dist/commands/setup-commands.js +2 -2
- package/dist/commands/sl-commands.js +19 -7
- package/dist/completion/complete-engine.d.ts +19 -0
- package/dist/completion/complete-engine.js +128 -0
- package/dist/completion/completion-scripts.d.ts +1 -0
- package/dist/completion/completion-scripts.js +36 -0
- package/dist/completion/dynamic-candidates.d.ts +6 -0
- package/dist/completion/dynamic-candidates.js +98 -0
- package/dist/connection-drivers.d.ts +3 -0
- package/dist/connection-drivers.js +17 -0
- package/dist/context/ingest/ingest-bundle.runner.d.ts +8 -0
- package/dist/context/ingest/ingest-bundle.runner.js +72 -15
- package/dist/context/ingest/ingest-profile.d.ts +102 -0
- package/dist/context/ingest/ingest-profile.js +306 -0
- package/dist/context/ingest/isolated-diff/work-unit-executor.js +25 -2
- package/dist/context/ingest/local-bundle-runtime.js +1 -0
- package/dist/context/ingest/local-ingest.d.ts +1 -1
- package/dist/context/ingest/local-ingest.js +6 -4
- package/dist/context/ingest/memory-flow/events.js +2 -1
- package/dist/context/ingest/ports.d.ts +2 -0
- package/dist/context/ingest/reports.d.ts +3 -0
- package/dist/context/ingest/reports.js +10 -0
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +3 -1
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -0
- package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +2 -1
- package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
- package/dist/context/ingest/tools/tool-call-logger.d.ts +6 -0
- package/dist/context/ingest/tools/tool-call-logger.js +36 -1
- package/dist/context/llm/ai-sdk-runtime.js +32 -3
- package/dist/context/llm/claude-code-runtime.js +51 -3
- package/dist/context/llm/runtime-port.d.ts +25 -0
- package/dist/context/mcp/context-tools.d.ts +2 -1
- package/dist/context/mcp/context-tools.js +82 -15
- package/dist/context/mcp/server.js +4 -0
- package/dist/context/mcp/types.d.ts +15 -1
- package/dist/context/project/config.d.ts +1 -0
- package/dist/context/project/config.js +4 -0
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/search/discover.js +4 -3
- package/dist/context/sl/local-sl.d.ts +15 -0
- package/dist/context/sl/local-sl.js +30 -0
- package/dist/context/wiki/local-knowledge.d.ts +10 -0
- package/dist/context/wiki/local-knowledge.js +22 -0
- package/dist/context-build-view.d.ts +0 -3
- package/dist/context-build-view.js +1 -7
- package/dist/ingest.js +7 -10
- package/dist/knowledge.d.ts +5 -0
- package/dist/knowledge.js +10 -1
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.d.ts +0 -7
- package/dist/public-ingest.js +20 -34
- package/dist/setup-context.js +6 -38
- package/dist/setup-databases.js +13 -82
- package/dist/setup-project.d.ts +0 -8
- package/dist/setup-project.js +3 -27
- package/dist/setup-sources.js +33 -5
- package/dist/setup.js +3 -16
- package/dist/skills/analytics/SKILL.md +6 -1
- package/dist/sl.d.ts +6 -1
- package/dist/sl.js +32 -8
- package/dist/telemetry/emitter.js +1 -1
- package/dist/telemetry/events.d.ts +4 -3
- package/dist/telemetry/events.js +7 -3
- package/dist/telemetry/identity.d.ts +1 -1
- package/dist/telemetry/identity.js +13 -10
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +5 -1
- package/package.json +22 -22
- package/dist/ingest-depth.d.ts +0 -8
- package/dist/ingest-depth.js +0 -56
- package/dist/setup-database-context-depth.d.ts +0 -23
- package/dist/setup-database-context-depth.js +0 -84
|
@@ -13,6 +13,7 @@ import { selectRelevantCanonicalPins } from './canonical-pins.js';
|
|
|
13
13
|
import { finalGateRepairPaths, repairFinalGateFailure } from './final-gate-repair.js';
|
|
14
14
|
import { compareFinalizationDeclarations, deriveFinalizationTouchedSources, deriveFinalizationWikiPageKeys, } from './finalization-scope.js';
|
|
15
15
|
import { FileIngestTraceWriter, ingestTracePathForJob, traceTimed } from './ingest-trace.js';
|
|
16
|
+
import { formatIngestProfile, formatIngestProfileJson, readIngestProfile, resolveIngestProfileMode } from './ingest-profile.js';
|
|
16
17
|
import { integrateWorkUnitPatch } from './isolated-diff/patch-integrator.js';
|
|
17
18
|
import { resolveTextualConflict } from './isolated-diff/textual-conflict-resolver.js';
|
|
18
19
|
import { runIsolatedWorkUnit } from './isolated-diff/work-unit-executor.js';
|
|
@@ -34,7 +35,7 @@ import { createEvictionListTool } from './tools/eviction-list.tool.js';
|
|
|
34
35
|
import { createReadRawSpanTool } from './tools/read-raw-span.tool.js';
|
|
35
36
|
import { createStageDiffTool } from './tools/stage-diff.tool.js';
|
|
36
37
|
import { createStageListTool } from './tools/stage-list.tool.js';
|
|
37
|
-
import { wrapToolsWithLogger } from './tools/tool-call-logger.js';
|
|
38
|
+
import { flushToolCallLogs, wrapToolsWithLogger } from './tools/tool-call-logger.js';
|
|
38
39
|
import { createMutableToolTranscriptSummary, recordToolTranscriptEntry, } from './tools/tool-transcript-summary.js';
|
|
39
40
|
import { repairWikiSlRefs } from './wiki-sl-ref-repair.js';
|
|
40
41
|
async function copyTransientIngestEvidence(sourceWorkdir, targetWorkdir) {
|
|
@@ -137,6 +138,36 @@ export class IngestBundleRunner {
|
|
|
137
138
|
ctx?.memoryFlow?.finish('error', [sanitizeMemoryFlowError(error)]);
|
|
138
139
|
throw error;
|
|
139
140
|
}
|
|
141
|
+
finally {
|
|
142
|
+
await this.maybeEmitIngestProfile(job.jobId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* When profiling is enabled — via the `KTX_PROFILE_INGEST` env var or the
|
|
147
|
+
* `ingest.profile` config setting — read the job's trace + tool transcripts
|
|
148
|
+
* and print a rolled-up timing breakdown to stderr. `json` emits the raw
|
|
149
|
+
* structured profile for coding agents; `table` emits a human summary.
|
|
150
|
+
* Best-effort: profiling never affects the run outcome.
|
|
151
|
+
*/
|
|
152
|
+
async maybeEmitIngestProfile(jobId) {
|
|
153
|
+
const mode = resolveIngestProfileMode(this.deps.settings.profileIngest);
|
|
154
|
+
if (mode === 'off') {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
// Tool transcripts are appended fire-and-forget; flush them so per-work-unit
|
|
159
|
+
// toolMs (and the derived model-vs-tool split) is complete before we read.
|
|
160
|
+
await flushToolCallLogs();
|
|
161
|
+
const storage = this.deps.storage;
|
|
162
|
+
const profile = await readIngestProfile(jobId, {
|
|
163
|
+
tracePath: storage.resolveTracePath?.(jobId) ?? ingestTracePathForJob(this.deps.storage.homeDir, jobId),
|
|
164
|
+
transcriptDir: this.deps.storage.resolveTranscriptDir(jobId),
|
|
165
|
+
});
|
|
166
|
+
process.stderr.write(`\n${mode === 'json' ? formatIngestProfileJson(profile) : formatIngestProfile(profile)}`);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
this.logger.warn(`[ingest-bundle] ingest profile unavailable for job=${jobId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
|
+
}
|
|
140
171
|
}
|
|
141
172
|
stageRawFilesStage1 = stageRawFilesStage1;
|
|
142
173
|
async syncKnowledgeSlRefsFromActions(connectionId, actions) {
|
|
@@ -789,7 +820,7 @@ export class IngestBundleRunner {
|
|
|
789
820
|
}));
|
|
790
821
|
const fetchReport = adapter.readFetchReport ? await adapter.readFetchReport(stagedDir) : null;
|
|
791
822
|
const scopeDescriptor = adapter.describeScope ? await adapter.describeScope(stagedDir) : null;
|
|
792
|
-
const sessionWorktree = await this.deps.lockingService.withLock('config:repo', () => this.deps.sessionWorktreeService.create(job.jobId, baseSha));
|
|
823
|
+
const sessionWorktree = await traceTimed(trace, 'worktree', 'session_worktree_created', { jobId: job.jobId }, () => this.deps.lockingService.withLock('config:repo', () => this.deps.sessionWorktreeService.create(job.jobId, baseSha)));
|
|
793
824
|
let cleanupOutcome = 'crash';
|
|
794
825
|
try {
|
|
795
826
|
activePhase = 'stage_raw_files';
|
|
@@ -916,26 +947,26 @@ export class IngestBundleRunner {
|
|
|
916
947
|
sourceContextReport = chunk.contextReport;
|
|
917
948
|
parseArtifacts = chunk.parseArtifacts;
|
|
918
949
|
reconcileNotes = chunk.reconcileNotes ?? [];
|
|
950
|
+
const pageTriage = this.deps.pageTriage;
|
|
951
|
+
const triageRunId = runRow.id;
|
|
919
952
|
triageResult =
|
|
920
|
-
contextReport && adapter.triageSupported &&
|
|
921
|
-
? await
|
|
953
|
+
contextReport && adapter.triageSupported && pageTriage
|
|
954
|
+
? await traceTimed(runTrace, 'triage', 'page_triage', { sourceKey: job.sourceKey }, () => pageTriage.triageRun({
|
|
922
955
|
stagedDir,
|
|
923
|
-
runId:
|
|
956
|
+
runId: triageRunId,
|
|
924
957
|
connectionId: job.connectionId,
|
|
925
958
|
sourceKey: job.sourceKey,
|
|
926
959
|
syncId,
|
|
927
960
|
jobId: job.jobId,
|
|
928
961
|
diffSet,
|
|
929
962
|
adapter,
|
|
930
|
-
})
|
|
963
|
+
}))
|
|
931
964
|
: null;
|
|
932
965
|
workUnits = this.filterWorkUnitsForTriage(workUnits, triageResult);
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
embedding: this.deps.embedding,
|
|
938
|
-
});
|
|
966
|
+
const clusterWorkUnits = adapter.clusterWorkUnits;
|
|
967
|
+
if (clusterWorkUnits && workUnits.length > 0) {
|
|
968
|
+
const preClusterCount = workUnits.length;
|
|
969
|
+
workUnits = await traceTimed(runTrace, 'clustering', 'cluster_work_units', { workUnitCount: preClusterCount }, () => clusterWorkUnits({ workUnits, stagedDir, embedding: this.deps.embedding }));
|
|
939
970
|
}
|
|
940
971
|
await stage2?.updateProgress(1.0, `Planned ${workUnits.length} update${workUnits.length === 1 ? '' : 's'}`);
|
|
941
972
|
}
|
|
@@ -968,7 +999,7 @@ export class IngestBundleRunner {
|
|
|
968
999
|
connectionIds: slConnectionIds,
|
|
969
1000
|
});
|
|
970
1001
|
// Build shared per-job context.
|
|
971
|
-
const [wikiIndex, slIndex] = await Promise.all([this.buildWikiIndex(), this.buildSlIndex(slConnectionIds)]);
|
|
1002
|
+
const [wikiIndex, slIndex] = await traceTimed(runTrace, 'index_build', 'build_indexes', { connectionCount: slConnectionIds.length }, () => Promise.all([this.buildWikiIndex(), this.buildSlIndex(slConnectionIds)]));
|
|
972
1003
|
const baseFraming = await this.deps.promptService.loadPrompt('memory_agent_bundle_ingest_work_unit');
|
|
973
1004
|
const wuSkillNames = Array.from(new Set([...adapter.skillNames, 'ingest_triage', 'sl_capture', 'wiki_capture']));
|
|
974
1005
|
const wuSkills = await this.deps.skillsRegistry.listSkills(wuSkillNames, 'memory_agent');
|
|
@@ -1420,6 +1451,8 @@ export class IngestBundleRunner {
|
|
|
1420
1451
|
let curatorReport = null;
|
|
1421
1452
|
let curatorWarnings = [];
|
|
1422
1453
|
let reconcileOutcome;
|
|
1454
|
+
const reconcileStartedAt = Date.now();
|
|
1455
|
+
const reconcileMode = contextReport && this.deps.curatorPagination ? 'curator' : 'single';
|
|
1423
1456
|
if (contextReport && this.deps.curatorPagination) {
|
|
1424
1457
|
const curatorOutcome = await this.deps.curatorPagination.reconcile({
|
|
1425
1458
|
runId: runRow.id,
|
|
@@ -1506,6 +1539,26 @@ export class IngestBundleRunner {
|
|
|
1506
1539
|
: undefined,
|
|
1507
1540
|
});
|
|
1508
1541
|
}
|
|
1542
|
+
await runTrace.event('debug', 'reconciliation', 'reconciliation_executed', {
|
|
1543
|
+
mode: reconcileMode,
|
|
1544
|
+
skipped: reconcileOutcome.skipped,
|
|
1545
|
+
...(reconcileOutcome.stopReason ? { stopReason: reconcileOutcome.stopReason } : {}),
|
|
1546
|
+
...(reconcileOutcome.metrics
|
|
1547
|
+
? {
|
|
1548
|
+
agentLoopMs: reconcileOutcome.metrics.totalMs,
|
|
1549
|
+
stepCount: reconcileOutcome.metrics.stepCount,
|
|
1550
|
+
...(reconcileOutcome.metrics.usage.inputTokens !== undefined
|
|
1551
|
+
? { inputTokens: reconcileOutcome.metrics.usage.inputTokens }
|
|
1552
|
+
: {}),
|
|
1553
|
+
...(reconcileOutcome.metrics.usage.outputTokens !== undefined
|
|
1554
|
+
? { outputTokens: reconcileOutcome.metrics.usage.outputTokens }
|
|
1555
|
+
: {}),
|
|
1556
|
+
...(reconcileOutcome.metrics.usage.totalTokens !== undefined
|
|
1557
|
+
? { totalTokens: reconcileOutcome.metrics.usage.totalTokens }
|
|
1558
|
+
: {}),
|
|
1559
|
+
}
|
|
1560
|
+
: {}),
|
|
1561
|
+
}, undefined, Date.now() - reconcileStartedAt);
|
|
1509
1562
|
latestReconciliationSkipped = reconcileOutcome.skipped;
|
|
1510
1563
|
const danglingReconcileWikiRefs = await findDanglingWikiRefsForActions({
|
|
1511
1564
|
wikiService: rcScopedWiki,
|
|
@@ -1544,6 +1597,7 @@ export class IngestBundleRunner {
|
|
|
1544
1597
|
activePhase = 'finalization';
|
|
1545
1598
|
if (adapter.finalize) {
|
|
1546
1599
|
const stageFinalization = ctx?.startPhase(0.04);
|
|
1600
|
+
const finalizationStartedAt = Date.now();
|
|
1547
1601
|
emitStageProgress('finalization', 87, 'Running deterministic finalization');
|
|
1548
1602
|
await stageFinalization?.updateProgress(0.0, 'Running deterministic finalization');
|
|
1549
1603
|
await runTrace.event('debug', 'finalization', 'finalization_started', { sourceKey: job.sourceKey });
|
|
@@ -1706,7 +1760,7 @@ export class IngestBundleRunner {
|
|
|
1706
1760
|
touchedSources: finalizationTouchedSources,
|
|
1707
1761
|
changedWikiPageKeys: finalizationChangedWikiPageKeys,
|
|
1708
1762
|
warnings: result.warnings,
|
|
1709
|
-
});
|
|
1763
|
+
}, undefined, Date.now() - finalizationStartedAt);
|
|
1710
1764
|
}
|
|
1711
1765
|
else {
|
|
1712
1766
|
await runTrace.event('debug', 'finalization', 'finalization_skipped', { sourceKey: job.sourceKey });
|
|
@@ -1933,6 +1987,7 @@ export class IngestBundleRunner {
|
|
|
1933
1987
|
const stage6 = ctx?.startPhase(0.04);
|
|
1934
1988
|
emitStageProgress('save', 91, 'Saving changes');
|
|
1935
1989
|
await stage6?.updateProgress(0.0, 'Saving changes');
|
|
1990
|
+
const squashStartedAt = Date.now();
|
|
1936
1991
|
try {
|
|
1937
1992
|
await sessionWorktree.git.assertWorktreeClean();
|
|
1938
1993
|
}
|
|
@@ -1955,7 +2010,7 @@ export class IngestBundleRunner {
|
|
|
1955
2010
|
await runTrace.event('debug', 'squash', 'squash_finished', {
|
|
1956
2011
|
commitSha,
|
|
1957
2012
|
touchedPaths: mergeResult.touchedPaths,
|
|
1958
|
-
});
|
|
2013
|
+
}, undefined, Date.now() - squashStartedAt);
|
|
1959
2014
|
const memoryFlowSavedActions = stageIndex.workUnits
|
|
1960
2015
|
.flatMap((wu) => wu.actions)
|
|
1961
2016
|
.concat(reconcileActions)
|
|
@@ -1971,6 +2026,7 @@ export class IngestBundleRunner {
|
|
|
1971
2026
|
// transaction. If this throws, the run fails and no partial index state
|
|
1972
2027
|
// survives (thanks to the transactional upsert in applyDiffTransactional).
|
|
1973
2028
|
if (commitSha) {
|
|
2029
|
+
const indexSyncStartedAt = Date.now();
|
|
1974
2030
|
// Multi-file squash → omit path so the handler diffs the whole commit
|
|
1975
2031
|
// (a comma-joined pathspec would match nothing and the job would no-op).
|
|
1976
2032
|
const pathFilter = mergeResult.touchedPaths.length === 1 ? mergeResult.touchedPaths[0] : '';
|
|
@@ -1992,6 +2048,7 @@ export class IngestBundleRunner {
|
|
|
1992
2048
|
this.logger.warn(`[ingest-bundle] post-squash SL reindex failed for connection=${connectionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1993
2049
|
}
|
|
1994
2050
|
}
|
|
2051
|
+
await runTrace.event('debug', 'index_sync', 'post_squash_index_sync_finished', { connectionCount: touchedConnections.length }, undefined, Date.now() - indexSyncStartedAt);
|
|
1995
2052
|
}
|
|
1996
2053
|
const stage5 = ctx?.startPhase(0.04);
|
|
1997
2054
|
emitStageProgress('provenance', 95, 'Recording history');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export interface IngestProfilePaths {
|
|
3
|
+
tracePath: string;
|
|
4
|
+
transcriptDir: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Post-processor over the ingest trace (`<home>/ingest-traces/<jobId>/trace.jsonl`)
|
|
8
|
+
* and per-work-unit tool transcripts. Turns the durations recorded during a run
|
|
9
|
+
* into a rolled-up "where did the time go" view. Gated for display by
|
|
10
|
+
* `KTX_PROFILE_INGEST`; the durations themselves are always written to the trace.
|
|
11
|
+
*/
|
|
12
|
+
declare const traceEventSchema: z.ZodObject<{
|
|
13
|
+
at: z.ZodOptional<z.ZodString>;
|
|
14
|
+
phase: z.ZodString;
|
|
15
|
+
event: z.ZodString;
|
|
16
|
+
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
18
|
+
}, z.core.$loose>;
|
|
19
|
+
/** @internal */
|
|
20
|
+
export type ProfiledTraceEvent = z.infer<typeof traceEventSchema>;
|
|
21
|
+
export interface IngestProfile {
|
|
22
|
+
jobId: string;
|
|
23
|
+
totalWallMs?: number;
|
|
24
|
+
phases: Array<{
|
|
25
|
+
phase: string;
|
|
26
|
+
totalMs: number;
|
|
27
|
+
/** Number of timed (durationMs-bearing) events that contributed to this phase. */
|
|
28
|
+
count: number;
|
|
29
|
+
}>;
|
|
30
|
+
workUnits: Array<{
|
|
31
|
+
unitKey: string;
|
|
32
|
+
status?: string;
|
|
33
|
+
/** Wall-clock for the whole work-unit run (agent loop + validation + git). */
|
|
34
|
+
totalMs?: number;
|
|
35
|
+
/** Pure `generateText` agent-loop time reported by the runtime. */
|
|
36
|
+
agentLoopMs?: number;
|
|
37
|
+
/** Summed tool-execution time from the work-unit transcript. */
|
|
38
|
+
toolMs?: number;
|
|
39
|
+
/** Derived model "thinking" time = agentLoopMs - toolMs (clamped at 0). */
|
|
40
|
+
modelMs?: number;
|
|
41
|
+
/** Worktree create time. */
|
|
42
|
+
createMs?: number;
|
|
43
|
+
/** Worktree teardown time. */
|
|
44
|
+
cleanupMs?: number;
|
|
45
|
+
stepCount?: number;
|
|
46
|
+
totalTokens?: number;
|
|
47
|
+
}>;
|
|
48
|
+
workUnitCount: number;
|
|
49
|
+
failedWorkUnitCount: number;
|
|
50
|
+
/**
|
|
51
|
+
* Plain-language diagnosis plus the raw numbers behind it, so a reader (human
|
|
52
|
+
* or coding agent) gets the conclusion without re-deriving it from the tables.
|
|
53
|
+
*/
|
|
54
|
+
summary: {
|
|
55
|
+
/** One-sentence conclusion, e.g. which phase dominated and whether work was model- or tool-bound. */
|
|
56
|
+
headline: string;
|
|
57
|
+
dominantPhase?: {
|
|
58
|
+
phase: string;
|
|
59
|
+
totalMs: number;
|
|
60
|
+
pctOfWall?: number;
|
|
61
|
+
};
|
|
62
|
+
/** Aggregate across all work units, in milliseconds. */
|
|
63
|
+
workUnits?: {
|
|
64
|
+
count: number;
|
|
65
|
+
failed: number;
|
|
66
|
+
agentLoopMs: number;
|
|
67
|
+
modelMs: number;
|
|
68
|
+
toolMs: number;
|
|
69
|
+
/** Percent of agent-loop time spent in model generation vs tool execution. */
|
|
70
|
+
modelPct?: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** @internal */
|
|
75
|
+
export declare function parseTraceEvents(traceText: string): ProfiledTraceEvent[];
|
|
76
|
+
/** @internal */
|
|
77
|
+
export declare function aggregateIngestProfile(input: {
|
|
78
|
+
jobId: string;
|
|
79
|
+
events: ProfiledTraceEvent[];
|
|
80
|
+
toolMsByUnit: Record<string, number>;
|
|
81
|
+
}): IngestProfile;
|
|
82
|
+
/** Read the trace and tool transcripts for a job and aggregate them into a profile. */
|
|
83
|
+
export declare function readIngestProfile(jobId: string, paths: IngestProfilePaths): Promise<IngestProfile>;
|
|
84
|
+
/** Render a human-readable profile table for stderr / the admin command. */
|
|
85
|
+
export declare function formatIngestProfile(profile: IngestProfile, options?: {
|
|
86
|
+
topWorkUnits?: number;
|
|
87
|
+
}): string;
|
|
88
|
+
/**
|
|
89
|
+
* Machine-readable rendering for coding agents: the full structured profile
|
|
90
|
+
* (raw milliseconds and token counts, stable keys) as a single JSON object
|
|
91
|
+
* under a stable marker line so it is easy to locate and parse in stderr.
|
|
92
|
+
*/
|
|
93
|
+
export declare function formatIngestProfileJson(profile: IngestProfile): string;
|
|
94
|
+
export type IngestProfileMode = 'off' | 'table' | 'json';
|
|
95
|
+
/**
|
|
96
|
+
* Resolve how (and whether) to emit the ingest profile, from the
|
|
97
|
+
* `ingest.profile` config value and the `KTX_PROFILE_INGEST` env var. Either
|
|
98
|
+
* source may request `json` (raw, agent-friendly) or a human `table`; `json`
|
|
99
|
+
* wins if either asks for it.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveIngestProfileMode(configValue: boolean | 'json' | undefined, env?: NodeJS.ProcessEnv): IngestProfileMode;
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
/**
|
|
5
|
+
* Post-processor over the ingest trace (`<home>/ingest-traces/<jobId>/trace.jsonl`)
|
|
6
|
+
* and per-work-unit tool transcripts. Turns the durations recorded during a run
|
|
7
|
+
* into a rolled-up "where did the time go" view. Gated for display by
|
|
8
|
+
* `KTX_PROFILE_INGEST`; the durations themselves are always written to the trace.
|
|
9
|
+
*/
|
|
10
|
+
const traceEventSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
at: z.string().optional(),
|
|
13
|
+
phase: z.string(),
|
|
14
|
+
event: z.string(),
|
|
15
|
+
durationMs: z.number().optional(),
|
|
16
|
+
data: z.record(z.string(), z.unknown()).optional(),
|
|
17
|
+
})
|
|
18
|
+
.loose();
|
|
19
|
+
function asNumber(value) {
|
|
20
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
function asString(value) {
|
|
23
|
+
return typeof value === 'string' ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
/** @internal */
|
|
26
|
+
export function parseTraceEvents(traceText) {
|
|
27
|
+
const events = [];
|
|
28
|
+
for (const line of traceText.split('\n')) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let json;
|
|
34
|
+
try {
|
|
35
|
+
json = JSON.parse(trimmed);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const parsed = traceEventSchema.safeParse(json);
|
|
41
|
+
if (parsed.success) {
|
|
42
|
+
events.push(parsed.data);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return events;
|
|
46
|
+
}
|
|
47
|
+
/** @internal */
|
|
48
|
+
export function aggregateIngestProfile(input) {
|
|
49
|
+
const { jobId, events, toolMsByUnit } = input;
|
|
50
|
+
const phaseTotals = new Map();
|
|
51
|
+
const workUnits = new Map();
|
|
52
|
+
const wu = (unitKey) => {
|
|
53
|
+
let existing = workUnits.get(unitKey);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
existing = { unitKey };
|
|
56
|
+
workUnits.set(unitKey, existing);
|
|
57
|
+
}
|
|
58
|
+
return existing;
|
|
59
|
+
};
|
|
60
|
+
let minAt = Number.POSITIVE_INFINITY;
|
|
61
|
+
let maxAt = Number.NEGATIVE_INFINITY;
|
|
62
|
+
for (const event of events) {
|
|
63
|
+
const at = event.at ? Date.parse(event.at) : Number.NaN;
|
|
64
|
+
if (!Number.isNaN(at)) {
|
|
65
|
+
minAt = Math.min(minAt, at);
|
|
66
|
+
maxAt = Math.max(maxAt, at);
|
|
67
|
+
}
|
|
68
|
+
if (event.durationMs !== undefined) {
|
|
69
|
+
const bucket = phaseTotals.get(event.phase) ?? { totalMs: 0, count: 0 };
|
|
70
|
+
bucket.totalMs += event.durationMs;
|
|
71
|
+
bucket.count += 1;
|
|
72
|
+
phaseTotals.set(event.phase, bucket);
|
|
73
|
+
}
|
|
74
|
+
const data = event.data ?? {};
|
|
75
|
+
const unitKey = asString(data.unitKey);
|
|
76
|
+
if (unitKey) {
|
|
77
|
+
const entry = wu(unitKey);
|
|
78
|
+
if (event.event === 'work_unit_executed') {
|
|
79
|
+
entry.totalMs = event.durationMs;
|
|
80
|
+
entry.agentLoopMs = asNumber(data.agentLoopMs);
|
|
81
|
+
entry.stepCount = asNumber(data.stepCount);
|
|
82
|
+
entry.totalTokens = asNumber(data.totalTokens);
|
|
83
|
+
entry.status = asString(data.status) ?? entry.status;
|
|
84
|
+
}
|
|
85
|
+
else if (event.event === 'work_unit_child_created') {
|
|
86
|
+
entry.createMs = event.durationMs;
|
|
87
|
+
}
|
|
88
|
+
else if (event.event === 'work_unit_child_cleanup') {
|
|
89
|
+
entry.cleanupMs = event.durationMs;
|
|
90
|
+
}
|
|
91
|
+
else if (event.event === 'work_unit_failed_before_patch') {
|
|
92
|
+
entry.status = entry.status ?? 'failed';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const [unitKey, entry] of workUnits) {
|
|
97
|
+
const toolMs = toolMsByUnit[unitKey];
|
|
98
|
+
if (toolMs !== undefined) {
|
|
99
|
+
entry.toolMs = toolMs;
|
|
100
|
+
if (entry.agentLoopMs !== undefined) {
|
|
101
|
+
entry.modelMs = Math.max(0, entry.agentLoopMs - toolMs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (entry.agentLoopMs !== undefined) {
|
|
105
|
+
entry.modelMs = entry.agentLoopMs;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const phases = [...phaseTotals.entries()]
|
|
109
|
+
.map(([phase, { totalMs, count }]) => ({ phase, totalMs, count }))
|
|
110
|
+
.sort((a, b) => b.totalMs - a.totalMs);
|
|
111
|
+
const workUnitList = [...workUnits.values()].sort((a, b) => (b.totalMs ?? 0) - (a.totalMs ?? 0));
|
|
112
|
+
const totalWallMs = Number.isFinite(minAt) && Number.isFinite(maxAt) && maxAt >= minAt ? maxAt - minAt : undefined;
|
|
113
|
+
const failedWorkUnitCount = workUnitList.filter((entry) => entry.status === 'failed').length;
|
|
114
|
+
return {
|
|
115
|
+
jobId,
|
|
116
|
+
...(totalWallMs !== undefined ? { totalWallMs } : {}),
|
|
117
|
+
phases,
|
|
118
|
+
workUnits: workUnitList,
|
|
119
|
+
workUnitCount: workUnitList.length,
|
|
120
|
+
failedWorkUnitCount,
|
|
121
|
+
summary: buildSummary(phases, workUnitList, failedWorkUnitCount, totalWallMs),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function buildSummary(phases, workUnits, failed, totalWallMs) {
|
|
125
|
+
const dominant = phases[0];
|
|
126
|
+
const dominantPhase = dominant
|
|
127
|
+
? {
|
|
128
|
+
phase: dominant.phase,
|
|
129
|
+
totalMs: dominant.totalMs,
|
|
130
|
+
...(totalWallMs && totalWallMs > 0
|
|
131
|
+
? { pctOfWall: Math.round((dominant.totalMs / totalWallMs) * 100) }
|
|
132
|
+
: {}),
|
|
133
|
+
}
|
|
134
|
+
: undefined;
|
|
135
|
+
const agentLoopMs = workUnits.reduce((sum, wu) => sum + (wu.agentLoopMs ?? 0), 0);
|
|
136
|
+
const toolMs = workUnits.reduce((sum, wu) => sum + (wu.toolMs ?? 0), 0);
|
|
137
|
+
const modelMs = workUnits.reduce((sum, wu) => sum + (wu.modelMs ?? 0), 0);
|
|
138
|
+
const workUnitAggregate = workUnits.length > 0
|
|
139
|
+
? {
|
|
140
|
+
count: workUnits.length,
|
|
141
|
+
failed,
|
|
142
|
+
agentLoopMs,
|
|
143
|
+
modelMs,
|
|
144
|
+
toolMs,
|
|
145
|
+
...(agentLoopMs > 0 ? { modelPct: Math.round((modelMs / agentLoopMs) * 100) } : {}),
|
|
146
|
+
}
|
|
147
|
+
: undefined;
|
|
148
|
+
const parts = [];
|
|
149
|
+
if (dominantPhase) {
|
|
150
|
+
const pct = dominantPhase.pctOfWall !== undefined ? `, ${dominantPhase.pctOfWall}% of wall time` : '';
|
|
151
|
+
parts.push(`Slowest phase: ${dominantPhase.phase} (${formatMs(dominantPhase.totalMs)}${pct})`);
|
|
152
|
+
}
|
|
153
|
+
if (workUnitAggregate) {
|
|
154
|
+
const split = workUnitAggregate.modelPct !== undefined
|
|
155
|
+
? `, ~${workUnitAggregate.modelPct}% model generation vs ~${100 - workUnitAggregate.modelPct}% tools`
|
|
156
|
+
: '';
|
|
157
|
+
parts.push(`${workUnitAggregate.count} work unit${workUnitAggregate.count === 1 ? '' : 's'}${failed > 0 ? ` (${failed} failed)` : ''}${split}`);
|
|
158
|
+
}
|
|
159
|
+
const headline = parts.length > 0 ? parts.join('. ') + '.' : 'No timed phases recorded.';
|
|
160
|
+
return {
|
|
161
|
+
headline,
|
|
162
|
+
...(dominantPhase ? { dominantPhase } : {}),
|
|
163
|
+
...(workUnitAggregate ? { workUnits: workUnitAggregate } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Read the trace and tool transcripts for a job and aggregate them into a profile. */
|
|
167
|
+
export async function readIngestProfile(jobId, paths) {
|
|
168
|
+
const traceText = await readFile(paths.tracePath, 'utf-8');
|
|
169
|
+
const events = parseTraceEvents(traceText);
|
|
170
|
+
const toolMsByUnit = await readToolMsByUnit(paths.transcriptDir);
|
|
171
|
+
return aggregateIngestProfile({ jobId, events, toolMsByUnit });
|
|
172
|
+
}
|
|
173
|
+
async function listTranscriptFiles(dir) {
|
|
174
|
+
// Work-unit keys can contain slashes (e.g. "cards/marketing"), so the runner
|
|
175
|
+
// writes nested transcript files (".../cards/marketing.jsonl"). Walk
|
|
176
|
+
// recursively and bucket by the `wuKey` field inside each entry rather than
|
|
177
|
+
// by file name.
|
|
178
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
179
|
+
if (!entries) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const files = [];
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const full = join(dir, entry.name);
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
files.push(...(await listTranscriptFiles(full)));
|
|
187
|
+
}
|
|
188
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
189
|
+
files.push(full);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return files;
|
|
193
|
+
}
|
|
194
|
+
async function readToolMsByUnit(transcriptDir) {
|
|
195
|
+
const toolMs = {};
|
|
196
|
+
for (const file of await listTranscriptFiles(transcriptDir)) {
|
|
197
|
+
let text;
|
|
198
|
+
try {
|
|
199
|
+
text = await readFile(file, 'utf-8');
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for (const line of text.split('\n')) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
if (!trimmed) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const entry = JSON.parse(trimmed);
|
|
211
|
+
const wuKey = asString(entry.wuKey);
|
|
212
|
+
const ms = asNumber(entry.durationMs);
|
|
213
|
+
if (wuKey && ms !== undefined) {
|
|
214
|
+
toolMs[wuKey] = (toolMs[wuKey] ?? 0) + ms;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// skip malformed line
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return toolMs;
|
|
223
|
+
}
|
|
224
|
+
function formatMs(ms) {
|
|
225
|
+
if (ms === undefined) {
|
|
226
|
+
return '—';
|
|
227
|
+
}
|
|
228
|
+
if (ms < 1000) {
|
|
229
|
+
return `${Math.round(ms)}ms`;
|
|
230
|
+
}
|
|
231
|
+
const seconds = ms / 1000;
|
|
232
|
+
if (seconds < 60) {
|
|
233
|
+
return `${seconds.toFixed(1)}s`;
|
|
234
|
+
}
|
|
235
|
+
const minutes = Math.floor(seconds / 60);
|
|
236
|
+
const rem = Math.round(seconds - minutes * 60);
|
|
237
|
+
return `${minutes}m ${String(rem).padStart(2, '0')}s`;
|
|
238
|
+
}
|
|
239
|
+
function formatTokens(tokens) {
|
|
240
|
+
if (tokens === undefined) {
|
|
241
|
+
return '—';
|
|
242
|
+
}
|
|
243
|
+
if (tokens < 1000) {
|
|
244
|
+
return String(tokens);
|
|
245
|
+
}
|
|
246
|
+
return `${(tokens / 1000).toFixed(1)}k`;
|
|
247
|
+
}
|
|
248
|
+
function pad(value, width) {
|
|
249
|
+
return value.length >= width ? value : value + ' '.repeat(width - value.length);
|
|
250
|
+
}
|
|
251
|
+
function padStart(value, width) {
|
|
252
|
+
return value.length >= width ? value : ' '.repeat(width - value.length) + value;
|
|
253
|
+
}
|
|
254
|
+
/** Render a human-readable profile table for stderr / the admin command. */
|
|
255
|
+
export function formatIngestProfile(profile, options = {}) {
|
|
256
|
+
const topWorkUnits = options.topWorkUnits ?? 10;
|
|
257
|
+
const lines = [];
|
|
258
|
+
lines.push(`ktx ingest profile — job ${profile.jobId}`);
|
|
259
|
+
if (profile.totalWallMs !== undefined) {
|
|
260
|
+
lines.push(` total wall time: ${formatMs(profile.totalWallMs)}`);
|
|
261
|
+
}
|
|
262
|
+
lines.push(` ${profile.summary.headline}`);
|
|
263
|
+
const wall = profile.totalWallMs;
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push(' Phase breakdown (by total duration):');
|
|
266
|
+
if (profile.phases.length === 0) {
|
|
267
|
+
lines.push(' (no timed phases recorded)');
|
|
268
|
+
}
|
|
269
|
+
for (const phase of profile.phases) {
|
|
270
|
+
const pct = wall && wall > 0 ? `(${((phase.totalMs / wall) * 100).toFixed(1)}%)` : '';
|
|
271
|
+
lines.push(` ${pad(phase.phase, 22)}${padStart(formatMs(phase.totalMs), 9)} ${padStart(pct, 8)} ${padStart(String(phase.count), 4)} event${phase.count === 1 ? '' : 's'}`);
|
|
272
|
+
}
|
|
273
|
+
if (profile.workUnits.length > 0) {
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push(` Work units (top ${Math.min(topWorkUnits, profile.workUnits.length)} slowest):`);
|
|
276
|
+
lines.push(` ${pad('unitKey', 30)}${padStart('total', 9)}${padStart('model', 9)}${padStart('tool', 9)}${padStart('steps', 8)}${padStart('tokens', 9)} status`);
|
|
277
|
+
for (const entry of profile.workUnits.slice(0, topWorkUnits)) {
|
|
278
|
+
const steps = entry.stepCount !== undefined ? String(entry.stepCount) : '—';
|
|
279
|
+
lines.push(` ${pad(entry.unitKey.slice(0, 30), 30)}${padStart(formatMs(entry.totalMs), 9)}${padStart(formatMs(entry.modelMs), 9)}${padStart(formatMs(entry.toolMs), 9)}${padStart(steps, 8)}${padStart(formatTokens(entry.totalTokens), 9)} ${entry.status ?? '—'}`);
|
|
280
|
+
}
|
|
281
|
+
lines.push(` (${profile.workUnitCount} work unit${profile.workUnitCount === 1 ? '' : 's'} total; ${profile.failedWorkUnitCount} failed)`);
|
|
282
|
+
}
|
|
283
|
+
return `${lines.join('\n')}\n`;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Machine-readable rendering for coding agents: the full structured profile
|
|
287
|
+
* (raw milliseconds and token counts, stable keys) as a single JSON object
|
|
288
|
+
* under a stable marker line so it is easy to locate and parse in stderr.
|
|
289
|
+
*/
|
|
290
|
+
export function formatIngestProfileJson(profile) {
|
|
291
|
+
return `ktx ingest profile (json)\n${JSON.stringify(profile, null, 2)}\n`;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Resolve how (and whether) to emit the ingest profile, from the
|
|
295
|
+
* `ingest.profile` config value and the `KTX_PROFILE_INGEST` env var. Either
|
|
296
|
+
* source may request `json` (raw, agent-friendly) or a human `table`; `json`
|
|
297
|
+
* wins if either asks for it.
|
|
298
|
+
*/
|
|
299
|
+
export function resolveIngestProfileMode(configValue, env = process.env) {
|
|
300
|
+
const envValue = env.KTX_PROFILE_INGEST;
|
|
301
|
+
if (configValue === 'json' || envValue === 'json') {
|
|
302
|
+
return 'json';
|
|
303
|
+
}
|
|
304
|
+
const wantsTable = configValue === true || envValue === '1' || envValue === 'true' || envValue === 'table';
|
|
305
|
+
return wantsTable ? 'table' : 'off';
|
|
306
|
+
}
|
|
@@ -8,15 +8,37 @@ function patchFileName(unitIndex, unitKey) {
|
|
|
8
8
|
export async function runIsolatedWorkUnit(input) {
|
|
9
9
|
const sessionKey = `${input.trace.context.jobId}-${input.workUnit.unitKey}`;
|
|
10
10
|
let cleanupOutcome = 'crash';
|
|
11
|
+
const createStartedAt = Date.now();
|
|
11
12
|
const child = await input.sessionWorktreeService.create(sessionKey, input.ingestionBaseSha);
|
|
12
13
|
await input.trace.event('debug', 'work_unit', 'work_unit_child_created', {
|
|
13
14
|
unitKey: input.workUnit.unitKey,
|
|
14
15
|
unitIndex: input.unitIndex,
|
|
15
16
|
worktreePath: child.workdir,
|
|
16
17
|
baseSha: input.ingestionBaseSha,
|
|
17
|
-
});
|
|
18
|
+
}, undefined, Date.now() - createStartedAt);
|
|
18
19
|
try {
|
|
20
|
+
const runStartedAt = Date.now();
|
|
19
21
|
const outcome = await input.run(child);
|
|
22
|
+
await input.trace.event('debug', 'work_unit', 'work_unit_executed', {
|
|
23
|
+
unitKey: input.workUnit.unitKey,
|
|
24
|
+
unitIndex: input.unitIndex,
|
|
25
|
+
status: outcome.status,
|
|
26
|
+
...(outcome.metrics
|
|
27
|
+
? {
|
|
28
|
+
agentLoopMs: outcome.metrics.totalMs,
|
|
29
|
+
stepCount: outcome.metrics.stepCount,
|
|
30
|
+
...(outcome.metrics.usage.inputTokens !== undefined
|
|
31
|
+
? { inputTokens: outcome.metrics.usage.inputTokens }
|
|
32
|
+
: {}),
|
|
33
|
+
...(outcome.metrics.usage.outputTokens !== undefined
|
|
34
|
+
? { outputTokens: outcome.metrics.usage.outputTokens }
|
|
35
|
+
: {}),
|
|
36
|
+
...(outcome.metrics.usage.totalTokens !== undefined
|
|
37
|
+
? { totalTokens: outcome.metrics.usage.totalTokens }
|
|
38
|
+
: {}),
|
|
39
|
+
}
|
|
40
|
+
: {}),
|
|
41
|
+
}, undefined, Date.now() - runStartedAt);
|
|
20
42
|
if (outcome.status !== 'success') {
|
|
21
43
|
cleanupOutcome = 'success';
|
|
22
44
|
await input.trace.event('error', 'work_unit', 'work_unit_failed_before_patch', {
|
|
@@ -51,11 +73,12 @@ export async function runIsolatedWorkUnit(input) {
|
|
|
51
73
|
throw error;
|
|
52
74
|
}
|
|
53
75
|
finally {
|
|
76
|
+
const cleanupStartedAt = Date.now();
|
|
54
77
|
await input.sessionWorktreeService.cleanup(child, cleanupOutcome);
|
|
55
78
|
await input.trace.event('trace', 'work_unit', 'work_unit_child_cleanup', {
|
|
56
79
|
unitKey: input.workUnit.unitKey,
|
|
57
80
|
outcome: cleanupOutcome,
|
|
58
81
|
worktreePath: child.workdir,
|
|
59
|
-
});
|
|
82
|
+
}, undefined, Date.now() - cleanupStartedAt);
|
|
60
83
|
}
|
|
61
84
|
}
|
|
@@ -574,6 +574,7 @@ export function createLocalBundleIngestRuntime(options) {
|
|
|
574
574
|
workUnitMaxConcurrency: options.project.config.ingest.workUnits.maxConcurrency,
|
|
575
575
|
workUnitStepBudget: options.project.config.ingest.workUnits.stepBudget,
|
|
576
576
|
workUnitFailureMode: options.project.config.ingest.workUnits.failureMode,
|
|
577
|
+
profileIngest: options.project.config.ingest.profile,
|
|
577
578
|
ingestTraceLevel: ingestTraceLevelFromEnv(),
|
|
578
579
|
},
|
|
579
580
|
skillsRegistry: new SkillsRegistryService({ skillsDir, logger }),
|
|
@@ -67,7 +67,7 @@ export interface LocalMetabaseFanoutProgress {
|
|
|
67
67
|
metabaseDatabaseId: number;
|
|
68
68
|
targetConnectionId: string;
|
|
69
69
|
jobId: string;
|
|
70
|
-
status: 'done' | 'failed';
|
|
70
|
+
status: 'done' | 'partial' | 'failed';
|
|
71
71
|
}): void;
|
|
72
72
|
}
|
|
73
73
|
export interface RunLocalMetabaseIngestOptions extends Omit<RunLocalIngestOptions, 'adapter' | 'connectionId' | 'sourceDir' | 'jobId'> {
|