| 456 | // --------------------------------------------------------------------------- |
| 457 | |
| 458 | export class MemoryService extends EventEmitter { |
| 459 | /** Serializes mutating commands per physical root (agent tool + UI writes). */ |
| 460 | private readonly locks = new MutexMap<string>(); |
| 461 | constructor( |
| 462 | private readonly config: Config, |
| 463 | /** Host-local sidecar for pins + usage stats, recorded at this chokepoint. */ |
| 464 | private readonly metaService: MemoryMetaService |
| 465 | ) { |
| 466 | super(); |
| 467 | } |
| 468 | |
| 469 | // ------------------------------------------------------------------------- |
| 470 | // Usage stats (sidecar): recorded here — the single chokepoint every agent |
| 471 | // command and UI write funnels through. UI reads (readFileWithSha) are |
| 472 | // intentionally not counted: stats track agent usage, not human browsing. |
| 473 | // Best-effort: stats failures must never break a memory command. |
| 474 | // ------------------------------------------------------------------------- |
| 475 | |
| 476 | /** Logical sidecar key, or null when the scope has no stable identity. */ |
| 477 | private logicalKeyFor(ctx: MemoryScopeContext, scope: MemoryScope, relPath: string) { |
| 478 | if (scope === "project" && ctx.projectPath === "") return null; |
| 479 | return memoryLogicalKey(scope, relPath, { |
| 480 | projectPath: ctx.projectPath, |
| 481 | workspaceId: ctx.workspaceId, |
| 482 | }); |
| 483 | } |
| 484 | |
| 485 | private async recordUsage( |
| 486 | ctx: MemoryScopeContext, |
| 487 | scope: MemoryScope, |
| 488 | relPath: string, |
| 489 | options: { write: boolean } |
| 490 | ): Promise<void> { |
| 491 | try { |
| 492 | const key = this.logicalKeyFor(ctx, scope, relPath); |
| 493 | if (key === null) return; |
| 494 | await this.metaService.recordAccess(key, options); |
| 495 | } catch (error) { |
| 496 | log.debug("[MemoryService] failed to record memory usage", { scope, relPath, error }); |
| 497 | } |
| 498 | } |
| 499 | |
| 500 | private async recordRename( |
| 501 | ctx: MemoryScopeContext, |
| 502 | scope: MemoryScope, |
| 503 | oldRelPath: string, |
| 504 | newRelPath: string |
| 505 | ): Promise<void> { |
| 506 | try { |
| 507 | const oldKey = this.logicalKeyFor(ctx, scope, oldRelPath); |
| 508 | const newKey = this.logicalKeyFor(ctx, scope, newRelPath); |
| 509 | if (oldKey === null || newKey === null) return; |
| 510 | // Pins and stats follow the file; the rename itself counts as a use. |
| 511 | await this.metaService.renameKeys(oldKey, newKey); |
| 512 | await this.metaService.recordAccess(newKey, { write: true }); |
| 513 | } catch (error) { |
| 514 | log.debug("[MemoryService] failed to move memory usage stats on rename", { |
| 515 | scope, |
nothing calls this directly
no outgoing calls
no test coverage detected