@kaelio/ktx 0.9.0 → 0.11.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.
Files changed (143) hide show
  1. package/assets/python/{kaelio_ktx-0.9.0-py3-none-any.whl → kaelio_ktx-0.11.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/clack.d.ts +6 -0
  5. package/dist/clack.js +17 -2
  6. package/dist/cli-program.d.ts +3 -0
  7. package/dist/cli-program.js +46 -2
  8. package/dist/cli-runtime.d.ts +5 -0
  9. package/dist/cli-runtime.js +50 -0
  10. package/dist/commands/setup-commands.js +2 -3
  11. package/dist/community-cta.d.ts +11 -0
  12. package/dist/community-cta.js +19 -0
  13. package/dist/connection.js +23 -1
  14. package/dist/connectors/bigquery/connector.d.ts +2 -5
  15. package/dist/connectors/bigquery/connector.js +2 -2
  16. package/dist/connectors/clickhouse/connector.d.ts +2 -5
  17. package/dist/connectors/clickhouse/connector.js +2 -2
  18. package/dist/connectors/mysql/connector.d.ts +7 -6
  19. package/dist/connectors/mysql/connector.js +25 -5
  20. package/dist/connectors/mysql/dialect.d.ts +1 -1
  21. package/dist/connectors/mysql/dialect.js +12 -2
  22. package/dist/connectors/postgres/connector.d.ts +2 -5
  23. package/dist/connectors/postgres/connector.js +2 -2
  24. package/dist/connectors/snowflake/connector.d.ts +2 -5
  25. package/dist/connectors/snowflake/connector.js +2 -2
  26. package/dist/connectors/sqlite/connector.d.ts +2 -5
  27. package/dist/connectors/sqlite/connector.js +2 -2
  28. package/dist/connectors/sqlserver/connector.d.ts +2 -5
  29. package/dist/connectors/sqlserver/connector.js +2 -2
  30. package/dist/context/connections/drivers.d.ts +0 -1
  31. package/dist/context/connections/drivers.js +0 -7
  32. package/dist/context/connections/query-executor.d.ts +2 -1
  33. package/dist/context/core/abort.d.ts +9 -0
  34. package/dist/context/core/abort.js +36 -0
  35. package/dist/context/core/git-env.d.ts +12 -1
  36. package/dist/context/core/git-env.js +17 -2
  37. package/dist/context/core/git.service.js +15 -7
  38. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +1 -0
  39. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +6 -2
  40. package/dist/context/ingest/context-candidates/curator-pagination.service.d.ts +1 -5
  41. package/dist/context/ingest/context-candidates/curator-pagination.service.js +1 -3
  42. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  43. package/dist/context/ingest/final-gate-repair.d.ts +1 -0
  44. package/dist/context/ingest/final-gate-repair.js +1 -0
  45. package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
  46. package/dist/context/ingest/ingest-bundle.runner.js +127 -53
  47. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +1 -0
  48. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +1 -0
  49. package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
  50. package/dist/context/ingest/local-bundle-runtime.js +11 -4
  51. package/dist/context/ingest/local-ingest.d.ts +1 -0
  52. package/dist/context/ingest/local-ingest.js +13 -3
  53. package/dist/context/ingest/memory-flow/events.js +1 -1
  54. package/dist/context/ingest/memory-flow/schema.js +8 -3
  55. package/dist/context/ingest/memory-flow/types.d.ts +7 -3
  56. package/dist/context/ingest/ports.d.ts +3 -5
  57. package/dist/context/ingest/stages/stage-3-work-units.d.ts +1 -4
  58. package/dist/context/ingest/stages/stage-3-work-units.js +5 -1
  59. package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +1 -4
  60. package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
  61. package/dist/context/ingest/types.d.ts +1 -0
  62. package/dist/context/llm/ai-sdk-runtime.d.ts +3 -0
  63. package/dist/context/llm/ai-sdk-runtime.js +152 -16
  64. package/dist/context/llm/claude-code-runtime.d.ts +6 -4
  65. package/dist/context/llm/claude-code-runtime.js +127 -48
  66. package/dist/context/llm/codex-runtime.d.ts +3 -3
  67. package/dist/context/llm/codex-runtime.js +90 -47
  68. package/dist/context/llm/local-config.d.ts +15 -5
  69. package/dist/context/llm/local-config.js +6 -1
  70. package/dist/context/llm/rate-limit-governor.d.ts +103 -0
  71. package/dist/context/llm/rate-limit-governor.js +285 -0
  72. package/dist/context/llm/runtime-port.d.ts +3 -6
  73. package/dist/context/mcp/context-tools.js +43 -13
  74. package/dist/context/project/config.d.ts +12 -0
  75. package/dist/context/project/config.js +35 -0
  76. package/dist/context/scan/types.d.ts +15 -2
  77. package/dist/context/scan/types.js +12 -0
  78. package/dist/context/sl/description-normalization.js +4 -14
  79. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  80. package/dist/context-build-view.d.ts +13 -0
  81. package/dist/context-build-view.js +60 -1
  82. package/dist/demo-metrics.d.ts +0 -2
  83. package/dist/demo-metrics.js +1 -11
  84. package/dist/ingest.d.ts +1 -0
  85. package/dist/ingest.js +32 -3
  86. package/dist/io/symbols.d.ts +2 -0
  87. package/dist/io/symbols.js +2 -0
  88. package/dist/io/tty.d.ts +9 -0
  89. package/dist/io/tty.js +5 -0
  90. package/dist/links.d.ts +1 -0
  91. package/dist/links.js +1 -0
  92. package/dist/memory-flow-hud.js +8 -16
  93. package/dist/public-ingest.js +50 -15
  94. package/dist/reveal-password-prompt.d.ts +24 -0
  95. package/dist/reveal-password-prompt.js +78 -0
  96. package/dist/scan.js +18 -2
  97. package/dist/setup-agents.js +1 -5
  98. package/dist/setup-databases.d.ts +1 -0
  99. package/dist/setup-databases.js +23 -3
  100. package/dist/setup-demo-tour.js +1 -0
  101. package/dist/setup-embeddings.js +1 -1
  102. package/dist/setup-models.d.ts +1 -14
  103. package/dist/setup-models.js +116 -340
  104. package/dist/setup-prompts.js +4 -7
  105. package/dist/setup-sources.js +7 -7
  106. package/dist/setup.d.ts +26 -1
  107. package/dist/setup.js +78 -7
  108. package/dist/sl.d.ts +2 -2
  109. package/dist/sl.js +20 -4
  110. package/dist/sql.js +18 -2
  111. package/dist/star-prompt/cache.d.ts +16 -0
  112. package/dist/star-prompt/cache.js +45 -0
  113. package/dist/star-prompt/star-count.d.ts +7 -0
  114. package/dist/star-prompt/star-count.js +66 -0
  115. package/dist/star-prompt/star-line.d.ts +12 -0
  116. package/dist/star-prompt/star-line.js +26 -0
  117. package/dist/telemetry/command-hook.d.ts +24 -0
  118. package/dist/telemetry/command-hook.js +37 -3
  119. package/dist/telemetry/emitter.d.ts +10 -0
  120. package/dist/telemetry/emitter.js +31 -0
  121. package/dist/telemetry/events.d.ts +24 -0
  122. package/dist/telemetry/events.js +15 -0
  123. package/dist/telemetry/exception.d.ts +18 -0
  124. package/dist/telemetry/exception.js +162 -0
  125. package/dist/telemetry/index.d.ts +4 -3
  126. package/dist/telemetry/index.js +3 -2
  127. package/dist/telemetry/redaction-secrets.d.ts +11 -0
  128. package/dist/telemetry/redaction-secrets.js +92 -0
  129. package/dist/update-check/cache.d.ts +21 -0
  130. package/dist/update-check/cache.js +38 -0
  131. package/dist/update-check/channel.d.ts +15 -0
  132. package/dist/update-check/channel.js +30 -0
  133. package/dist/update-check/registry.d.ts +1 -0
  134. package/dist/update-check/registry.js +45 -0
  135. package/dist/update-check/update-check.d.ts +43 -0
  136. package/dist/update-check/update-check.js +116 -0
  137. package/package.json +8 -1
  138. package/dist/context/connections/local-query-executor.d.ts +0 -6
  139. package/dist/context/connections/local-query-executor.js +0 -39
  140. package/dist/context/connections/postgres-query-executor.d.ts +0 -25
  141. package/dist/context/connections/postgres-query-executor.js +0 -53
  142. package/dist/context/connections/sqlite-query-executor.d.ts +0 -4
  143. package/dist/context/connections/sqlite-query-executor.js +0 -74
@@ -115,6 +115,10 @@ export class IngestBundleRunner {
115
115
  this.logger = deps.logger ?? noopLogger;
116
116
  }
117
117
  async run(job, ctx) {
118
+ const unsubscribeRateLimitGovernor = this.subscribeRateLimitGovernor({
119
+ trace: this.createTrace(job),
120
+ memoryFlow: ctx?.memoryFlow,
121
+ });
118
122
  const key = job.connectionId;
119
123
  const previous = this.chainByConnection.get(key);
120
124
  if (previous) {
@@ -139,9 +143,64 @@ export class IngestBundleRunner {
139
143
  throw error;
140
144
  }
141
145
  finally {
146
+ unsubscribeRateLimitGovernor();
142
147
  await this.maybeEmitIngestProfile(job.jobId);
143
148
  }
144
149
  }
150
+ formatRateLimitWait(state) {
151
+ const seconds = Math.ceil(state.remainingMs / 1_000);
152
+ const minutes = Math.floor(seconds / 60);
153
+ const remainder = seconds % 60;
154
+ const duration = minutes > 0 ? `${minutes}m${String(remainder).padStart(2, '0')}s` : `${seconds}s`;
155
+ const type = state.rateLimitType ? ` ${state.rateLimitType}` : '';
156
+ return `Rate-limited (${state.provider}${type}); resuming in ${duration}; Ctrl+C to stop`;
157
+ }
158
+ subscribeRateLimitGovernor(input) {
159
+ const governor = this.deps.settings.rateLimitGovernor;
160
+ if (!governor) {
161
+ return () => undefined;
162
+ }
163
+ return governor.subscribe((state) => {
164
+ if (state.kind === 'rate_limit_observed') {
165
+ void input.trace.event('info', 'rate_limit', 'rate_limit_observed', { ...state });
166
+ return;
167
+ }
168
+ if (state.kind === 'concurrency_adjusted') {
169
+ void input.trace.event('info', 'rate_limit', 'concurrency_adjusted', { ...state });
170
+ return;
171
+ }
172
+ void input.trace.event('info', 'rate_limit', state.kind, { ...state });
173
+ if (state.kind === 'wait_tick' || state.kind === 'wait_started') {
174
+ input.memoryFlow?.emit({
175
+ type: 'rate_limit_wait',
176
+ provider: state.provider,
177
+ ...(state.rateLimitType ? { rateLimitType: state.rateLimitType } : {}),
178
+ resumeAtMs: state.resumeAtMs,
179
+ remainingMs: state.remainingMs,
180
+ });
181
+ input.memoryFlow?.emit({
182
+ type: 'stage_progress',
183
+ stage: 'integration',
184
+ percent: 50,
185
+ message: this.formatRateLimitWait(state),
186
+ transient: true,
187
+ });
188
+ }
189
+ });
190
+ }
191
+ async withRateLimitWorkSlot(abortSignal, fn) {
192
+ const governor = this.deps.settings.rateLimitGovernor;
193
+ if (!governor) {
194
+ return fn();
195
+ }
196
+ const release = await governor.acquireWorkSlot(abortSignal);
197
+ try {
198
+ return await fn();
199
+ }
200
+ finally {
201
+ release();
202
+ }
203
+ }
145
204
  /**
146
205
  * When profiling is enabled — via the `KTX_PROFILE_INGEST` env var or the
147
206
  * `ingest.profile` config setting — read the job's trace + tool transcripts
@@ -711,7 +770,6 @@ export class IngestBundleRunner {
711
770
  type: 'work_unit_started',
712
771
  unitKey: input.wu.unitKey,
713
772
  skills: input.wuSkillNames,
714
- stepBudget: input.workUnitSettings.stepBudget,
715
773
  });
716
774
  return executeWorkUnit({
717
775
  sessionWorktreeGit: input.worktree.git,
@@ -731,14 +789,30 @@ export class IngestBundleRunner {
731
789
  slIndex: input.slIndex,
732
790
  priorProvenance: input.priorProvenance,
733
791
  }),
734
- buildToolSet: (wuInner) => wrapToolsWithLogger(buildWuToolSet({
735
- sourceKey: input.job.sourceKey,
736
- stagedDir: input.stagedDir,
737
- wu: wuInner,
738
- loadSkillTool,
739
- emitUnmappedFallbackTool: wuEmitUnmappedFallbackTool,
740
- toolsetTools: wuToolset.toRuntimeTools(wuToolContext),
741
- }), join(input.transcriptDir, `${wuInner.unitKey}.jsonl`), wuInner.unitKey, { onEntry: input.recordTranscriptEntry(join(input.transcriptDir, `${wuInner.unitKey}.jsonl`)) }),
792
+ buildToolSet: (wuInner) => {
793
+ const transcriptPath = join(input.transcriptDir, `${wuInner.unitKey}.jsonl`);
794
+ const record = input.recordTranscriptEntry(transcriptPath);
795
+ return wrapToolsWithLogger(buildWuToolSet({
796
+ sourceKey: input.job.sourceKey,
797
+ stagedDir: input.stagedDir,
798
+ wu: wuInner,
799
+ loadSkillTool,
800
+ emitUnmappedFallbackTool: wuEmitUnmappedFallbackTool,
801
+ toolsetTools: wuToolset.toRuntimeTools(wuToolContext),
802
+ }), transcriptPath, wuInner.unitKey, {
803
+ // Drive the live HUD heartbeat from real tool calls: each invocation
804
+ // ticks the running per-unit count. This is an observed signal, not a
805
+ // re-derived turn count, so it can never overshoot a budget.
806
+ onEntry: (entry) => {
807
+ const summary = record(entry);
808
+ input.memoryFlow?.emit({
809
+ type: 'work_unit_step',
810
+ unitKey: wuInner.unitKey,
811
+ toolCalls: summary.toolCallCount,
812
+ });
813
+ },
814
+ });
815
+ },
742
816
  captureSession: session,
743
817
  sessionActions,
744
818
  modelRole: 'candidateExtraction',
@@ -747,7 +821,7 @@ export class IngestBundleRunner {
747
821
  connectionId: input.job.connectionId,
748
822
  jobId: input.job.jobId,
749
823
  toolFailureCount: (unitKey) => input.transcriptSummaries.get(unitKey)?.fatalErrorCount ?? 0,
750
- onStepFinish: input.onStepFinish,
824
+ abortSignal: input.abortSignal,
751
825
  }, input.wu);
752
826
  }
753
827
  async runInner(job, ctx) {
@@ -797,6 +871,7 @@ export class IngestBundleRunner {
797
871
  const current = transcriptSummaries.get(entry.wuKey) ?? createMutableToolTranscriptSummary(entry.wuKey, path);
798
872
  recordToolTranscriptEntry(current, entry);
799
873
  transcriptSummaries.set(entry.wuKey, current);
874
+ return current;
800
875
  };
801
876
  const overrideReport = await this.loadOverrideReport(job);
802
877
  const stage1 = ctx?.startPhase(0.08);
@@ -1111,7 +1186,7 @@ export class IngestBundleRunner {
1111
1186
  await stage3?.updateProgress(1.0, '0 of 0 work units complete');
1112
1187
  }
1113
1188
  try {
1114
- await Promise.all(workUnits.map((wu, index) => limitWorkUnit(async () => {
1189
+ await Promise.all(workUnits.map((wu, index) => limitWorkUnit(() => this.withRateLimitWorkSlot(ctx?.abortSignal, async () => {
1115
1190
  const outcome = await runIsolatedWorkUnit({
1116
1191
  unitIndex: index,
1117
1192
  ingestionBaseSha,
@@ -1119,6 +1194,7 @@ export class IngestBundleRunner {
1119
1194
  patchDir,
1120
1195
  trace: runTrace,
1121
1196
  workUnit: wu,
1197
+ abortSignal: ctx?.abortSignal,
1122
1198
  afterSuccess: (child) => copyTransientIngestEvidence(child.workdir, sessionWorktree.workdir),
1123
1199
  run: async (child) => {
1124
1200
  const scopedWikiService = this.deps.wikiService.forWorktree(child.workdir);
@@ -1147,11 +1223,9 @@ export class IngestBundleRunner {
1147
1223
  stageIndex,
1148
1224
  includeContextEvidenceTools: adapter.evidenceIndexing === 'documents' && !!contextReport,
1149
1225
  currentTableExists: (tableRef) => this.tableRefExistsInSemanticLayer(scopedSemanticLayerService, slConnectionIds, tableRef),
1226
+ abortSignal: ctx?.abortSignal,
1150
1227
  memoryFlow,
1151
1228
  wuSkillNames,
1152
- onStepFinish: ({ stepIndex, stepBudget }) => {
1153
- memoryFlow?.emit({ type: 'work_unit_step', unitKey: wu.unitKey, stepIndex, stepBudget });
1154
- },
1155
1229
  });
1156
1230
  },
1157
1231
  });
@@ -1173,7 +1247,7 @@ export class IngestBundleRunner {
1173
1247
  });
1174
1248
  completedWorkUnits += 1;
1175
1249
  await stage3?.updateProgress(completedWorkUnits / workUnits.length, `${completedWorkUnits} of ${workUnits.length} work units complete`);
1176
- })));
1250
+ }))));
1177
1251
  }
1178
1252
  catch (error) {
1179
1253
  await this.deps.runs.markFailed(runRow.id);
@@ -1250,6 +1324,7 @@ export class IngestBundleRunner {
1250
1324
  reason: context.reason,
1251
1325
  maxAttempts: 1,
1252
1326
  stepBudget: 12,
1327
+ abortSignal: ctx?.abortSignal,
1253
1328
  });
1254
1329
  emitStageProgress('integration', 82, result.status === 'repaired'
1255
1330
  ? `Resolved text conflict for ${context.unitKey}`
@@ -1267,6 +1342,7 @@ export class IngestBundleRunner {
1267
1342
  repairKind: 'patch_semantic_gate',
1268
1343
  maxAttempts: 1,
1269
1344
  stepBudget: 16,
1345
+ abortSignal: ctx?.abortSignal,
1270
1346
  });
1271
1347
  emitStageProgress('integration', 83, result.status === 'repaired'
1272
1348
  ? `Repaired semantic gate for ${context.unitKey}`
@@ -1451,6 +1527,37 @@ export class IngestBundleRunner {
1451
1527
  let curatorReport = null;
1452
1528
  let curatorWarnings = [];
1453
1529
  let reconcileOutcome;
1530
+ // Reconcile shares the work-unit liveness model: the HUD heartbeat is driven
1531
+ // by real tool calls (a monotonic, observed count), not a re-derived turn
1532
+ // counter. The soft cap only paces the phase progress bar; it is never shown
1533
+ // to the user, so it cannot read as a misleading "X/Y" fraction.
1534
+ const reconcileTranscriptPath = join(transcriptDir, 'reconcile.jsonl');
1535
+ const reconcileProgressSoftCap = 40;
1536
+ const buildReconcileToolSetWithHeartbeat = () => {
1537
+ const record = recordTranscriptEntry(reconcileTranscriptPath);
1538
+ return wrapToolsWithLogger(buildReconcileToolSet({
1539
+ loadSkillTool: rcLoadSkill,
1540
+ stageListTool: rcStageListTool,
1541
+ stageDiffTool: rcStageDiffTool,
1542
+ evictionListTool: rcEvictionListTool,
1543
+ emitConflictResolutionTool: rcEmitConflictResolutionTool,
1544
+ emitEvictionDecisionTool: rcEmitEvictionDecisionTool,
1545
+ emitArtifactResolutionTool: rcEmitArtifactResolutionTool,
1546
+ emitUnmappedFallbackTool: rcEmitUnmappedFallbackTool,
1547
+ readRawSpanTool: rcRawSpanTool,
1548
+ toolsetTools: rcToolset.toRuntimeTools(rcToolContext),
1549
+ }), reconcileTranscriptPath, 'reconcile', {
1550
+ onEntry: (entry) => {
1551
+ const summary = record(entry);
1552
+ if (!stage4) {
1553
+ return;
1554
+ }
1555
+ const label = `Reconciling results · ${summary.toolCallCount} action${summary.toolCallCount === 1 ? '' : 's'}`;
1556
+ emitStageProgress('reconciliation', 85, label, { transient: true });
1557
+ void stage4.updateProgress(Math.min(0.95, summary.toolCallCount / reconcileProgressSoftCap), label);
1558
+ },
1559
+ });
1560
+ };
1454
1561
  const reconcileStartedAt = Date.now();
1455
1562
  const reconcileMode = contextReport && this.deps.curatorPagination ? 'curator' : 'single';
1456
1563
  if (contextReport && this.deps.curatorPagination) {
@@ -1471,25 +1578,9 @@ export class IngestBundleRunner {
1471
1578
  canonicalPins: relevantCanonicalPins,
1472
1579
  }),
1473
1580
  buildUserPrompt: ({ summary, items, runState }) => buildReconcileUserPrompt(stageIndex, eviction, { summary, items }, reconcileNotes, runState),
1474
- buildToolSet: (_passNumber) => wrapToolsWithLogger(buildReconcileToolSet({
1475
- loadSkillTool: rcLoadSkill,
1476
- stageListTool: rcStageListTool,
1477
- stageDiffTool: rcStageDiffTool,
1478
- evictionListTool: rcEvictionListTool,
1479
- emitConflictResolutionTool: rcEmitConflictResolutionTool,
1480
- emitEvictionDecisionTool: rcEmitEvictionDecisionTool,
1481
- emitArtifactResolutionTool: rcEmitArtifactResolutionTool,
1482
- emitUnmappedFallbackTool: rcEmitUnmappedFallbackTool,
1483
- readRawSpanTool: rcRawSpanTool,
1484
- toolsetTools: rcToolset.toRuntimeTools(rcToolContext),
1485
- }), join(transcriptDir, 'reconcile.jsonl'), 'reconcile', { onEntry: recordTranscriptEntry(join(transcriptDir, 'reconcile.jsonl')) }),
1581
+ buildToolSet: (_passNumber) => buildReconcileToolSetWithHeartbeat(),
1486
1582
  getReconciliationActions: () => reconcileActions,
1487
- onStepFinish: stage4
1488
- ? ({ passNumber, stepIndex, stepBudget }) => {
1489
- emitStageProgress('reconciliation', 85, `Reconciling results: pass ${passNumber} step ${stepIndex}/${stepBudget}`, { transient: true });
1490
- void stage4.updateProgress(stepIndex / stepBudget, `Reconciling results · pass ${passNumber} step ${stepIndex}`);
1491
- }
1492
- : undefined,
1583
+ abortSignal: ctx?.abortSignal,
1493
1584
  });
1494
1585
  curatorReport = curatorOutcome.report;
1495
1586
  curatorWarnings = curatorOutcome.warnings;
@@ -1512,31 +1603,13 @@ export class IngestBundleRunner {
1512
1603
  canonicalPins: relevantCanonicalPins,
1513
1604
  }),
1514
1605
  buildUserPrompt: (idx, ev) => buildReconcileUserPrompt(idx, ev, undefined, reconcileNotes),
1515
- buildToolSet: () => wrapToolsWithLogger(buildReconcileToolSet({
1516
- loadSkillTool: rcLoadSkill,
1517
- stageListTool: rcStageListTool,
1518
- stageDiffTool: rcStageDiffTool,
1519
- evictionListTool: rcEvictionListTool,
1520
- emitConflictResolutionTool: rcEmitConflictResolutionTool,
1521
- emitEvictionDecisionTool: rcEmitEvictionDecisionTool,
1522
- emitArtifactResolutionTool: rcEmitArtifactResolutionTool,
1523
- emitUnmappedFallbackTool: rcEmitUnmappedFallbackTool,
1524
- readRawSpanTool: rcRawSpanTool,
1525
- toolsetTools: rcToolset.toRuntimeTools(rcToolContext),
1526
- }), join(transcriptDir, 'reconcile.jsonl'), 'reconcile', { onEntry: recordTranscriptEntry(join(transcriptDir, 'reconcile.jsonl')) }),
1606
+ buildToolSet: () => buildReconcileToolSetWithHeartbeat(),
1527
1607
  modelRole: 'reconcile',
1528
1608
  stepBudget: 60,
1529
1609
  sourceKey: job.sourceKey,
1530
1610
  jobId: job.jobId,
1531
1611
  force: !!overrideReport,
1532
- onStepFinish: stage4
1533
- ? ({ stepIndex, stepBudget }) => {
1534
- emitStageProgress('reconciliation', 85, `Reconciling results: step ${stepIndex}/${stepBudget}`, {
1535
- transient: true,
1536
- });
1537
- void stage4.updateProgress(stepIndex / stepBudget, `Reconciling results · step ${stepIndex}`);
1538
- }
1539
- : undefined,
1612
+ abortSignal: ctx?.abortSignal,
1540
1613
  });
1541
1614
  }
1542
1615
  await runTrace.event('debug', 'reconciliation', 'reconciliation_executed', {
@@ -1890,6 +1963,7 @@ export class IngestBundleRunner {
1890
1963
  repairKind: 'final_artifact_gate',
1891
1964
  maxAttempts: 1,
1892
1965
  stepBudget: 16,
1966
+ abortSignal: ctx?.abortSignal,
1893
1967
  });
1894
1968
  isolatedDiffSummary.gateRepairAttempts += gateRepair.attempts;
1895
1969
  if (gateRepair.status === 'failed') {
@@ -19,5 +19,6 @@ export interface ResolveTextualConflictInput {
19
19
  reason: string;
20
20
  maxAttempts?: number;
21
21
  stepBudget?: number;
22
+ abortSignal?: AbortSignal;
22
23
  }
23
24
  export declare function resolveTextualConflict(input: ResolveTextualConflictInput): Promise<TextualConflictResolutionResult>;
@@ -165,6 +165,7 @@ export async function resolveTextualConflict(input) {
165
165
  jobId: input.trace.context.jobId,
166
166
  unitKey: input.unitKey,
167
167
  },
168
+ abortSignal: input.abortSignal,
168
169
  }));
169
170
  if (result.stopReason === 'error') {
170
171
  lastFailure = result.error?.message ?? 'resolver agent loop errored';
@@ -9,6 +9,7 @@ export interface RunIsolatedWorkUnitInput {
9
9
  patchDir: string;
10
10
  trace: IngestTraceWriter;
11
11
  workUnit: WorkUnit;
12
+ abortSignal?: AbortSignal;
12
13
  run(child: IngestSessionWorktree): Promise<WorkUnitOutcome>;
13
14
  afterSuccess?(child: IngestSessionWorktree): Promise<void>;
14
15
  }
@@ -8,6 +8,7 @@ import { SessionWorktreeService } from '../../context/core/session-worktree.serv
8
8
  import { createRuntimeToolDescriptorFromAiTool } from '../../context/llm/runtime-tools.js';
9
9
  import { createLocalKtxLlmRuntimeFromConfig } from '../../context/llm/local-config.js';
10
10
  import { KtxIngestEmbeddingPortAdapter } from '../../context/llm/embedding-port.js';
11
+ import { createRateLimitGovernorConfig, RateLimitGovernor } from '../../context/llm/rate-limit-governor.js';
11
12
  import { RuntimeAgentRunner } from '../../context/llm/runtime-port.js';
12
13
  import { ktxLocalStateDbPath } from '../../context/project/local-state-db.js';
13
14
  import { PromptService } from '../../context/prompts/prompt.service.js';
@@ -491,15 +492,16 @@ function localIngestLlmProviderGuardMessage(projectDir) {
491
492
  'ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, claude-code, or codex, or an injected agentRunner.',
492
493
  'Configure a local Claude Code/Codex session or API-backed LLM, then rerun ingest:',
493
494
  ` ktx setup --project-dir ${projectDir} --llm-backend claude-code --no-input`,
494
- ` ktx setup --project-dir ${projectDir} --llm-backend codex --llm-model gpt-5.5 --no-input`,
495
- ` ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --llm-model claude-sonnet-4-6 --no-input`,
495
+ ` ktx setup --project-dir ${projectDir} --llm-backend codex --no-input`,
496
+ ` ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --no-input`,
496
497
  ].join('\n');
497
498
  }
498
- function resolveAgentRunner(options) {
499
+ function resolveAgentRunner(options, rateLimitGovernor) {
499
500
  const llmRuntime = options.llmRuntime ??
500
501
  (options.createLlmRuntime ?? createLocalKtxLlmRuntimeFromConfig)(options.project.config.llm, {
501
502
  projectDir: options.project.projectDir,
502
503
  env: process.env,
504
+ rateLimitGovernor,
503
505
  }) ??
504
506
  undefined;
505
507
  if (options.agentRunner) {
@@ -536,7 +538,11 @@ export function createLocalBundleIngestRuntime(options) {
536
538
  const knowledgeIndex = new LocalKnowledgeIndex(options.project, embedding);
537
539
  const knowledgeEvents = new NoopKnowledgeEventPort();
538
540
  const wikiService = new KnowledgeWikiService(rootFileStore, embedding, knowledgeIndex, options.project.git, logger);
539
- const { agentRunner, llmRuntime } = resolveAgentRunner(options);
541
+ const rateLimitGovernor = new RateLimitGovernor(createRateLimitGovernorConfig({
542
+ ...options.project.config.ingest.rateLimit,
543
+ maxConcurrency: options.project.config.ingest.workUnits.maxConcurrency,
544
+ }));
545
+ const { agentRunner, llmRuntime } = resolveAgentRunner(options, rateLimitGovernor);
540
546
  const promptService = new PromptService({ promptsDir, partials: [], logger });
541
547
  const storage = new LocalIngestStorage(options.project);
542
548
  const registry = registerAdapters(options.adapters);
@@ -575,6 +581,7 @@ export function createLocalBundleIngestRuntime(options) {
575
581
  workUnitMaxConcurrency: options.project.config.ingest.workUnits.maxConcurrency,
576
582
  workUnitStepBudget: options.project.config.ingest.workUnits.stepBudget,
577
583
  workUnitFailureMode: options.project.config.ingest.workUnits.failureMode,
584
+ rateLimitGovernor,
578
585
  profileIngest: options.project.config.ingest.profile,
579
586
  ingestTraceLevel: ingestTraceLevelFromEnv(),
580
587
  },
@@ -25,6 +25,7 @@ export interface RunLocalIngestOptions {
25
25
  queryExecutor?: KtxSqlQueryExecutorPort;
26
26
  logger?: KtxLogger;
27
27
  embeddingProvider?: import('../../llm/types.js').KtxEmbeddingProvider | null;
28
+ abortSignal?: AbortSignal;
28
29
  }
29
30
  export interface LocalIngestResult {
30
31
  result: IngestBundleResult;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { cp, mkdir, rm } from 'node:fs/promises';
3
3
  import { isAbsolute, resolve } from 'node:path';
4
+ import { createAbortError, isAbortError } from '../../context/core/abort.js';
4
5
  import { ktxLocalStateDbPath } from '../../context/project/local-state-db.js';
5
6
  import { planMetabaseFanoutChildren } from './adapters/metabase/fanout-planner.js';
6
7
  import { KtxYamlMetabaseSourceStateReader, LocalMetabaseDiscoveryCache } from './adapters/metabase/local-source-state-store.js';
@@ -36,10 +37,11 @@ function findAdapter(adapters, source) {
36
37
  }
37
38
  return adapter;
38
39
  }
39
- function localJobContext(jobId, memoryFlow) {
40
+ function localJobContext(jobId, memoryFlow, abortSignal) {
40
41
  return {
41
42
  jobId,
42
43
  ...(memoryFlow ? { memoryFlow } : {}),
44
+ ...(abortSignal ? { abortSignal } : {}),
43
45
  startPhase() {
44
46
  return new LocalIngestPhase();
45
47
  },
@@ -62,7 +64,7 @@ async function runScheduledPullJob(options) {
62
64
  sourceKey: options.adapter.source,
63
65
  trigger: options.trigger ?? 'manual_resync',
64
66
  bundleRef: { kind: 'scheduled_pull', config: options.pullConfig },
65
- }, localJobContext(jobId, options.memoryFlow));
67
+ }, localJobContext(jobId, options.memoryFlow, options.abortSignal));
66
68
  const report = await runtime.store.findByJobId(jobId);
67
69
  if (!report) {
68
70
  throw new Error(`Local ingest report for job "${jobId}" was not created`);
@@ -102,6 +104,7 @@ export async function runLocalIngest(options) {
102
104
  queryExecutor: options.queryExecutor,
103
105
  logger: options.logger,
104
106
  embeddingProvider: options.embeddingProvider,
107
+ abortSignal: options.abortSignal,
105
108
  });
106
109
  }
107
110
  const result = await runtime.runner.run({
@@ -110,7 +113,7 @@ export async function runLocalIngest(options) {
110
113
  sourceKey: adapter.source,
111
114
  trigger: options.trigger ?? (options.sourceDir ? 'upload' : 'manual_resync'),
112
115
  bundleRef,
113
- }, localJobContext(jobId, options.memoryFlow));
116
+ }, localJobContext(jobId, options.memoryFlow, options.abortSignal));
114
117
  const report = await runtime.store.findByJobId(jobId);
115
118
  if (!report) {
116
119
  throw new Error(`Local ingest report for job "${jobId}" was not created`);
@@ -226,6 +229,9 @@ export async function runLocalMetabaseIngest(options) {
226
229
  });
227
230
  const children = [];
228
231
  for (const childPlan of childPlans) {
232
+ if (options.abortSignal?.aborted) {
233
+ throw createAbortError();
234
+ }
229
235
  const targetConnectionId = safeSegment('target connection id', childPlan.targetConnectionId);
230
236
  if (!options.project.config.connections[targetConnectionId]) {
231
237
  throw new Error(`Target connection "${targetConnectionId}" is not configured in ktx.yaml`);
@@ -255,9 +261,13 @@ export async function runLocalMetabaseIngest(options) {
255
261
  queryExecutor: options.queryExecutor,
256
262
  logger: options.logger,
257
263
  embeddingProvider: options.embeddingProvider,
264
+ abortSignal: options.abortSignal,
258
265
  });
259
266
  }
260
267
  catch (error) {
268
+ if (isAbortError(error)) {
269
+ throw error;
270
+ }
261
271
  child = await recordLocalMetabaseChildFailure({
262
272
  project: options.project,
263
273
  jobId: childJobId,
@@ -127,7 +127,7 @@ export function ingestReportToMemoryFlowReplay(report, options = {}) {
127
127
  }
128
128
  const actions = allReportActions(report);
129
129
  const workUnitEvents = report.body.workUnits.flatMap((workUnit) => [
130
- { type: 'work_unit_started', unitKey: workUnit.unitKey, skills: [], stepBudget: 0 },
130
+ { type: 'work_unit_started', unitKey: workUnit.unitKey, skills: [] },
131
131
  ...workUnit.actions.map((action) => ({
132
132
  type: 'candidate_action',
133
133
  unitKey: workUnit.unitKey,
@@ -64,17 +64,22 @@ const memoryFlowEventSchema = z.discriminatedUnion('type', [
64
64
  message: z.string().min(1),
65
65
  transient: z.boolean().optional(),
66
66
  }),
67
+ eventSchema({
68
+ type: z.literal('rate_limit_wait'),
69
+ provider: z.string(),
70
+ rateLimitType: z.string().optional(),
71
+ resumeAtMs: z.number().int().nonnegative(),
72
+ remainingMs: z.number().int().nonnegative(),
73
+ }),
67
74
  eventSchema({
68
75
  type: z.literal('work_unit_started'),
69
76
  unitKey: z.string().min(1),
70
77
  skills: z.array(z.string().min(1)),
71
- stepBudget: z.number().int().min(0),
72
78
  }),
73
79
  eventSchema({
74
80
  type: z.literal('work_unit_step'),
75
81
  unitKey: z.string().min(1),
76
- stepIndex: z.number().int().min(0),
77
- stepBudget: z.number().int().min(0),
82
+ toolCalls: z.number().int().min(0),
78
83
  }),
79
84
  eventSchema({
80
85
  type: z.literal('candidate_action'),
@@ -44,16 +44,20 @@ type MemoryFlowEventPayload = {
44
44
  percent: number;
45
45
  message: string;
46
46
  transient?: boolean;
47
+ } | {
48
+ type: 'rate_limit_wait';
49
+ provider: string;
50
+ rateLimitType?: string;
51
+ resumeAtMs: number;
52
+ remainingMs: number;
47
53
  } | {
48
54
  type: 'work_unit_started';
49
55
  unitKey: string;
50
56
  skills: string[];
51
- stepBudget: number;
52
57
  } | {
53
58
  type: 'work_unit_step';
54
59
  unitKey: string;
55
- stepIndex: number;
56
- stepBudget: number;
60
+ toolCalls: number;
57
61
  } | {
58
62
  type: 'candidate_action';
59
63
  unitKey: string;
@@ -5,6 +5,7 @@ import type { KtxFileStorePort } from '../../context/core/file-store.js';
5
5
  import type { KtxLogger } from '../../context/core/config.js';
6
6
  import type { SessionOutcome } from '../../context/core/session-worktree.service.js';
7
7
  import type { AgentRunnerPort, KtxLlmRuntimePort, KtxRuntimeToolSet } from '../../context/llm/runtime-port.js';
8
+ import type { RateLimitGovernor } from '../llm/rate-limit-governor.js';
8
9
  import type { MemoryAction, MemoryKnowledgeSlRefsPort } from '../../context/memory/types.js';
9
10
  import type { PromptService } from '../../context/prompts/prompt.service.js';
10
11
  import type { SkillsRegistryService } from '../../context/skills/skills-registry.service.js';
@@ -111,6 +112,7 @@ interface IngestSettingsPort {
111
112
  workUnitMaxConcurrency?: number;
112
113
  workUnitStepBudget?: number;
113
114
  workUnitFailureMode?: 'abort' | 'continue';
115
+ rateLimitGovernor?: RateLimitGovernor;
114
116
  /** Print a timing breakdown to stderr at the end of each run (config-driven; see also KTX_PROFILE_INGEST). `'json'` emits the raw structured profile. */
115
117
  profileIngest?: boolean | 'json';
116
118
  ingestTraceLevel?: IngestTraceLevel;
@@ -282,11 +284,7 @@ export interface CuratorPaginationPort {
282
284
  }) => string;
283
285
  buildToolSet: (passNumber: number) => KtxRuntimeToolSet;
284
286
  getReconciliationActions: () => MemoryAction[];
285
- onStepFinish?: (info: {
286
- passNumber: number;
287
- stepIndex: number;
288
- stepBudget: number;
289
- }) => void;
287
+ abortSignal?: AbortSignal;
290
288
  }): Promise<ReconciliationOutcome & {
291
289
  report: CuratorPaginationReport;
292
290
  warnings: string[];
@@ -25,10 +25,7 @@ export interface WorkUnitExecutionDeps {
25
25
  sourceKey: string;
26
26
  connectionId: string;
27
27
  jobId: string;
28
- onStepFinish?: (info: {
29
- stepIndex: number;
30
- stepBudget: number;
31
- }) => void;
28
+ abortSignal?: AbortSignal;
32
29
  toolFailureCount?: (unitKey: string) => number;
33
30
  }
34
31
  export interface WorkUnitOutcome {
@@ -1,3 +1,4 @@
1
+ import { isAbortError } from '../../core/abort.js';
1
2
  import { listTouchedSlSources } from '../../../context/tools/touched-sl-sources.js';
2
3
  const MAX_WORK_UNIT_PROMPT_CHARS = 240_000;
3
4
  export async function executeWorkUnit(deps, wu) {
@@ -51,10 +52,13 @@ export async function executeWorkUnit(deps, wu) {
51
52
  unitKey: wu.unitKey,
52
53
  jobId: deps.jobId,
53
54
  },
54
- onStepFinish: deps.onStepFinish,
55
+ abortSignal: deps.abortSignal,
55
56
  });
56
57
  }
57
58
  catch (error) {
59
+ if (isAbortError(error)) {
60
+ throw error;
61
+ }
58
62
  return failWithResetFromCurrentHead(error instanceof Error ? error.message : String(error));
59
63
  }
60
64
  const postSha = (await deps.sessionWorktreeGit.revParseHead()) ?? preSha;
@@ -14,10 +14,7 @@ export interface ReconciliationContext {
14
14
  sourceKey: string;
15
15
  jobId: string;
16
16
  force?: boolean;
17
- onStepFinish?: (info: {
18
- stepIndex: number;
19
- stepBudget: number;
20
- }) => void;
17
+ abortSignal?: AbortSignal;
21
18
  forceRun?: boolean;
22
19
  }
23
20
  export interface ReconciliationOutcome {
@@ -11,7 +11,7 @@ export async function runReconciliationStage4(ctx) {
11
11
  toolSet: ctx.buildToolSet(),
12
12
  stepBudget: ctx.stepBudget,
13
13
  telemetryTags: { operationName: 'ingest-bundle-reconcile', source: ctx.sourceKey, jobId: ctx.jobId },
14
- onStepFinish: ctx.onStepFinish,
14
+ abortSignal: ctx.abortSignal,
15
15
  });
16
16
  return { skipped: false, stopReason: run.stopReason, error: run.error, ...(run.metrics ? { metrics: run.metrics } : {}) };
17
17
  }
@@ -198,6 +198,7 @@ export interface IngestJobPhase {
198
198
  export interface IngestJobContext {
199
199
  jobId: string;
200
200
  memoryFlow?: MemoryFlowEventSink;
201
+ abortSignal?: AbortSignal;
201
202
  startPhase(weight: number): IngestJobPhase;
202
203
  }
203
204
  export {};
@@ -3,6 +3,7 @@ import { type TelemetrySettings } from 'ai';
3
3
  import type { z } from 'zod';
4
4
  import { type KtxLogger } from '../../context/core/config.js';
5
5
  import { type KtxLlmDebugRequestRecorder } from './debug-request-recorder.js';
6
+ import type { RateLimitGovernor } from './rate-limit-governor.js';
6
7
  import type { KtxGenerateObjectInput, KtxGenerateTextInput, KtxLlmRuntimePort, RunLoopParams, RunLoopResult } from './runtime-port.js';
7
8
  interface AgentTelemetryPort {
8
9
  createTelemetry(tags: Record<string, string>): TelemetrySettings;
@@ -12,11 +13,13 @@ export interface AiSdkKtxLlmRuntimeDeps {
12
13
  telemetry?: AgentTelemetryPort;
13
14
  logger?: KtxLogger;
14
15
  debugRequestRecorder?: KtxLlmDebugRequestRecorder;
16
+ rateLimitGovernor?: Pick<RateLimitGovernor, 'waitForReady' | 'report' | 'maxRetryAttempts'>;
15
17
  }
16
18
  export declare class AiSdkKtxLlmRuntime implements KtxLlmRuntimePort {
17
19
  private readonly deps;
18
20
  private readonly logger;
19
21
  constructor(deps: AiSdkKtxLlmRuntimeDeps);
22
+ private generateTextWithRateLimitRetry;
20
23
  generateText(input: KtxGenerateTextInput): Promise<string>;
21
24
  generateObject<TOutput, TSchema extends z.ZodType<TOutput>>(input: KtxGenerateObjectInput<TOutput, TSchema>): Promise<TOutput>;
22
25
  runAgentLoop(params: RunLoopParams): Promise<RunLoopResult>;