task-context-tracker.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import path from 'node:path';
  2. import type { ContextFile } from '../../utils';
  3. interface PendingContextFile {
  4. path: string;
  5. lines: Set<number>;
  6. lastReadAt: number;
  7. }
  8. export function createTaskContextTracker() {
  9. const contextByTask = new Map<string, Map<string, PendingContextFile>>();
  10. const pendingManagedTaskIds = new Set<string>();
  11. return {
  12. pendingManagedTaskIds,
  13. addContext(taskId: string, files: ContextFile[]) {
  14. if (files.length === 0) return;
  15. let context = contextByTask.get(taskId);
  16. if (!context) {
  17. context = new Map();
  18. contextByTask.set(taskId, context);
  19. }
  20. for (const file of files) {
  21. const pending = context.get(file.path) ?? {
  22. path: file.path,
  23. lines: new Set<number>(),
  24. lastReadAt: file.lastReadAt,
  25. };
  26. for (const line of file.lineNumbers ?? []) {
  27. pending.lines.add(line);
  28. }
  29. pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
  30. context.set(file.path, pending);
  31. }
  32. },
  33. canTrack(taskId: string, backgroundJobBoard: { taskIDs(): Set<string> }) {
  34. return (
  35. pendingManagedTaskIds.has(taskId) ||
  36. backgroundJobBoard.taskIDs().has(taskId)
  37. );
  38. },
  39. prune(backgroundJobBoard: { taskIDs(): Set<string> }) {
  40. const remembered = backgroundJobBoard.taskIDs();
  41. for (const taskId of contextByTask.keys()) {
  42. if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
  43. contextByTask.delete(taskId);
  44. }
  45. }
  46. },
  47. clearSession(sessionId: string) {
  48. contextByTask.delete(sessionId);
  49. pendingManagedTaskIds.delete(sessionId);
  50. },
  51. contextFilesForPrompt(taskId: string): ContextFile[] {
  52. const context = contextByTask.get(taskId);
  53. if (!context) return [];
  54. return [...context.values()].map((file) => ({
  55. path: file.path,
  56. lineCount: file.lines.size,
  57. lastReadAt: file.lastReadAt,
  58. }));
  59. },
  60. };
  61. }
  62. export function extractReadFiles(
  63. root: string,
  64. output: { output: unknown; metadata?: unknown },
  65. ): ContextFile[] {
  66. if (typeof output.output !== 'string') return [];
  67. const extractPath = /<path>([^<]+)<\/path>/.exec(output.output)?.[1];
  68. if (!extractPath) return [];
  69. const relative = path.relative(root, extractPath);
  70. const normalized =
  71. !relative || relative.startsWith('..') || path.isAbsolute(relative)
  72. ? extractPath
  73. : relative;
  74. const matchedLines = new Set<number>();
  75. for (const match of output.output.matchAll(/^([0-9]+):/gm)) {
  76. matchedLines.add(Number(match[1]));
  77. }
  78. const lineNumbers = [...matchedLines];
  79. return [
  80. {
  81. path: normalized,
  82. lineCount: lineNumbers.length,
  83. lineNumbers,
  84. lastReadAt: Date.now(),
  85. },
  86. ];
  87. }