feat(core): make path-local instruction discovery durable

Replace synthetic-message instruction injection with a durable
discovery projection so discovered AGENTS.md files survive compaction,
forks, reverts, and restarts.

- Add the instruction_file table and the durable
  session.instructions.discovered event; projection stores each
  discovered path and content with its owning assistant-message
  boundary and durable discovery order.
- Fold discovered files into the core/instructions source through
  InstructionDiscovery, absorbing SessionInstructions. Completed
  compaction rebaselines restate discovered instructions instead of
  summarizing them away.
- Re-read discovered files live at each observation so mid-session
  edits reach the model; the frozen discovery content stands in only
  when a file becomes unreadable.
- Narrate instruction file changes as per-file deltas via diffByKey,
  restating the full set only for pure reorderings.
- Fork copies discoveries within the inherited transcript, committed
  revert removes discoveries past the revert boundary, and Session
  movement clears them so the destination initializes a complete
  baseline.
This commit is contained in:
Kit Langton 2026-07-06 14:22:53 -04:00
commit 0726f09b05
28 changed files with 1166 additions and 598 deletions

View file

@ -73,8 +73,10 @@ const model = OpenAIChat.route
})
.model({ id: "gpt-4o-mini" })
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) })
const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) })
const instructionBuiltIns = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) })
const instructionDiscovery = Layer.mock(InstructionDiscovery.Service, {
load: (_sessionID) => Effect.succeed(Instructions.empty),
})
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
@ -83,8 +85,8 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
[Snapshot.node, Snapshot.noopLayer],
[LayerNodePlatform.llmClient, client],
[SessionRunnerModel.node, models],
[InstructionBuiltIns.node, systemContext],
[InstructionDiscovery.node, instructionContext],
[InstructionBuiltIns.node, instructionBuiltIns],
[InstructionDiscovery.node, instructionDiscovery],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],
@ -133,8 +135,8 @@ const it = testEffect(
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
[SessionRunnerModel.node, models],
[InstructionBuiltIns.node, systemContext],
[InstructionDiscovery.node, instructionContext],
[InstructionBuiltIns.node, instructionBuiltIns],
[InstructionDiscovery.node, instructionDiscovery],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],