chore: merge dev into v2 (#34317)

Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com>
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Ben Guthrie <benjee.012@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
Kit Langton 2026-06-28 11:30:38 -04:00 committed by GitHub
commit 41283933ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
208 changed files with 9815 additions and 6651 deletions

View file

@ -1,235 +0,0 @@
# Layer Node Tiers
## Goal
`LayerNode` describes the complete dependency graph while allowing groups of nodes to be constructed with different lifecycle boundaries. The abstraction must not hard-code concepts such as global or location services.
## Node Definition
Nodes have an Effect service tag, a layer, dependencies, and a tier. The service tag's runtime `key` identifies the node in diagnostics:
```ts
export const node = LayerNode.make({
service: Watcher.Service,
layer,
deps: [],
tier: location,
})
```
Tier-specific makers supply the tier automatically:
```ts
export const node = makeLocationNode({
service: Watcher.Service,
layer,
deps: [Config.node, Git.node],
})
```
## Tier Declaration
Tiers are declared bottom-up, from the most specific lifecycle to the most foundational:
```ts
const tiers = LayerNode.tiers(["location", "global"])
```
An earlier tier may depend on its own tier or any later tier. A later tier cannot depend on an earlier tier.
For the example above:
- `location` may depend on `location` or `global`.
- `global` may depend only on `global`.
### Cross-Tier Dependencies
Dependencies on a later, more foundational tier are hoisted outside the current tier's construction boundary. For example, global dependencies of location nodes must be built outside a location `Layer.fresh` boundary.
From the perspective of a lower tier, each service crossing into a higher tier must resolve to one unique node identity:
- Multiple consumers may depend on the same higher-tier node.
- Two different higher-tier nodes for the same service are a conflict, even if both satisfy the same dependency type.
- This constraint applies to transitive higher-tier dependencies as well as direct dependencies.
This validation happens while traversing dependency edges for the single complete-graph topological sort. It is not reconstructed from the flattened sorted list and is not a separate validation pass.
The traversal must retain the lower-tier perspective when following a dependency into a higher tier. For each lower tier, it tracks the higher-tier node selected for every service key. Reaching the same service through the same node identity is valid; reaching it through a different node identity is a conflict.
Topological visitation and boundary validation are distinct traversal state:
- A node is emitted into the topological order once.
- A higher-tier node may need boundary validation once for each lower tier from which it is reachable.
This distinction is required for transitive dependencies. A higher-tier node may already be topologically visited when another lower-tier branch reaches it, but that later branch must still participate in service uniqueness validation.
The tier configuration generates correctly constrained makers:
```ts
export const makeLocationNode = tiers.make("location")
export const makeGlobalNode = tiers.make("global")
```
This must reject invalid dependencies at compile time:
```ts
makeGlobalNode({
service: Database.Service,
layer,
deps: [locationNode], // type error
})
```
## Building
`buildLayer` remains a top-level `LayerNode` function and receives the tier configuration:
```ts
const appLayer = LayerNode.buildLayer(root, tiers)
```
It performs these steps:
1. Traverse and topologically sort the complete reachable graph once, dependency-first.
2. While traversing dependency edges, detect cycles, validate tier direction, and validate unique cross-tier service implementations from each lower tier's perspective.
3. Partition the one sorted list by tier while preserving its relative order.
4. Process tiers in their declared bottom-up order.
5. Build one layer for each tier.
6. Connect tier layers with `Layer.provideMerge` according to tier dependencies.
7. Return one final closed Effect layer.
### One Topological Sort
There is one topological sort for the entire graph, not one sort per tier. Every reachable node is emitted into the sorted result once. Boundary-validation state may separately process a higher-tier node once per originating lower tier; this does not create another topological sort.
For example, one dependency-first result may be:
```text
[globalDatabase, globalGit, locationConfig, locationWatcher]
```
Stable partitioning then produces:
```text
global: [globalDatabase, globalGit]
location: [locationConfig, locationWatcher]
```
Because partitioning preserves relative order, dependencies within each tier remain before their consumers. Cross-tier dependencies were already validated while their dependency edges were available during traversal; validation is not attempted from the partitioned lists.
### Provider Rebinding Within A Tier
Topological node deduplication is not sufficient when a tier contains different nodes that provide the same service. The final linear layer plan must preserve which implementation each consumer depends on.
For example:
```text
ConsumerX -> X provides Service
ConsumerY -> Y provides Service
ConsumerX2 -> X provides Service
```
The resulting dependency-first plan must be able to represent:
```text
ConsumerX, X, ConsumerY, Y, ConsumerX2, X
```
After `Y` becomes the active implementation, the later dependency on `X` must emit `X` again. A global visited set must not incorrectly remove that second placement.
While constructing a tier's linear plan, track the active provider node for each service key:
- If the required provider is already active, its repeated placement may be omitted.
- If a different provider for the same service is active, emit the required provider again and make it active.
- If no provider is active, emit the provider and make it active.
Repeated placement of the same node does not imply repeated resource acquisition. Effect layer memoization may still reuse the same layer instance. The repeated placement restores the intended provider binding for subsequent consumers.
This differs from cross-tier uniqueness. Multiple implementations may be rebound within one tier, but different implementations of the same service cannot both be hoisted across a tier boundary.
Without a custom build function, a tier's sorted layers are combined with the default `Layer.provideMerge` behavior.
## Custom Tier Build Function
The optional third argument customizes how each tier's sorted layers are constructed:
```ts
const appLayer = LayerNode.buildLayer(root, tiers, (tier, layers) => {
const combined = LayerNode.combine(layers)
if (tier !== "location") return combined
return Layer.effect(
LocationServiceMap,
LayerMap.make((ref: Location.Ref) => combined.pipe(Layer.provide(Location.layer(ref)), Layer.fresh), {
idleTimeToLive: "60 minutes",
}),
)
})
```
The callback receives:
- The tier name.
- The tier's layers in the dependency-first order preserved from the single complete-graph topological sort.
It returns the final layer representing that tier. This permits a tier to introduce a lifecycle boundary, wrap its layers in a `LayerMap`, or otherwise transform how the tier is built.
## Replacements
Tests and alternate runtimes may replace a specific layer implementation by exact object identity:
```ts
const layer = LayerNode.buildLayer(root, tiers, buildTier, [LayerNode.replace(Config.layer, testConfigLayer)])
```
The replacement applies to every placement of that exact source layer in the generated plans. Unused replacements are not acquired. A replacement must provide the same service output, must not introduce new errors, and must not have unresolved dependencies.
## Freshness
Global implementations must remain outside the location freshness boundary. Conceptually:
```ts
locationTier.pipe(Layer.fresh).pipe(Layer.provideMerge(globalTier))
```
The location tier contains only location implementations. Global dependencies are connected after the location build function creates its fresh or `LayerMap` boundary, so global services remain shared.
## Responsibilities
`LayerNode` owns:
- Service tags and dependency edges.
- Tier declarations and type-safe tier makers.
- Cycle detection and diagnostics using service keys such as `Watcher.Service.key`.
- One dependency-first topological sort of the complete graph.
- Cross-tier service uniqueness validation during dependency traversal, tracked per originating lower tier.
- Stable partitioning of the sorted nodes by tier.
- Provider-aware linearization within each tier, including rebinding when different nodes provide the same service.
- Invoking the optional tier build function.
- Wiring the resulting tier layers into one final layer.
The caller owns:
- The meaning of each tier.
- Tier-specific lifecycle behavior.
- Specialized wrappers such as `LocationServiceMap`.
The abstraction must not contain built-in knowledge of global, location, request, workspace, or other application-specific tiers.
## Deferred: packages/opencode Compatibility
The first implementation will not migrate or redesign the existing `packages/opencode` integration with core's `LocationServiceMap`.
`packages/opencode` currently uses its own `InstanceState` lifecycle while bridging to core location services through `LocationServiceMap`. Production consumers include:
- `packages/opencode/src/session/system.ts`
- `packages/opencode/src/agent/agent.ts`
- `packages/opencode/src/cli/cmd/debug/file.ts`
- `packages/opencode/src/cli/cmd/debug/v2.ts`
- `packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts`
- `packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts`
Some consumers wrap `LocationServiceMap.layer` as an opaque `LayerNode`; others provide the layer directly. We need to determine how these bridges consume the tier-built core graph and how unresolved global dependencies are exposed after the new core location builder is implemented.
This compatibility work must happen after the first tier implementation. The first implementation should preserve existing `packages/opencode` behavior and avoid changing these bridges.

View file

@ -1,5 +1,12 @@
# V2 Schema Changelog
## 2026-06-26: Add Finite Session History
- Add `GET /api/session/:sessionID/history` and generated Promise, Effect, and legacy JavaScript client methods.
- Page public durable Session events after an optional exclusive aggregate sequence, with an explicit `hasMore` exhaustion signal.
- Keep aggregate gaps legal, cap pages at 100 events, and preserve the existing durable replay-and-tail `sessions.events()` stream unchanged.
- Add no migration or durable-event version; this is a finite read API over the existing event manifest.
## 2026-06-22: Simplify Session Input Promotion
- Keep `session.next.prompt.admitted.1` as the durable, client-visible record of pending Session input.

View file

@ -176,6 +176,10 @@ The synchronized `session.next.*` event family and projected Session-message mod
The first `sessions.events(...)` contract is durable-only during both replay and live tailing. This keeps one cursor equal to one persisted aggregate sequence and is sufficient for reconnect-safe consumers. A later UI-facing API may optionally interleave live-only deltas while connected, but those fragments must remain explicitly ephemeral: they cannot advance the durable cursor, replay after reconnect, or be mistaken for publication boundaries.
`sessions.history({ sessionID, after?, limit? })` is the finite counterpart for request/response consumers. `after` is an exclusive aggregate sequence, and omission starts before sequence zero. The response is `{ data, hasMore }`; callers derive the next `after` from the final event's durable sequence when `hasMore` is true. Public durable Session events are selected before pagination, which permits gaps from private or historical aggregate events while preserving strictly increasing unique sequences. The log has a moving head, so events committed between pages may appear on the next page.
The finite endpoint is `GET /api/session/:sessionID/history`, uses the normal Session Location and authorization middleware, defaults to 50 events, and accepts at most 100. It returns only events in the public durable Session schema. The existing `sessions.events()` replay-and-tail stream is unchanged.
Durable event tail wakeups are advisory and edge-triggered. Each active tail owns one sliding-capacity-1 dirty signal for its aggregate and re-queries SQLite after a wake. Repeated commits coalesce while the tail is busy because durable rows, not in-memory notifications, preserve every event and sequence. Subscribe and register the dirty signal before historical replay, then remove it when the tail closes, so replay handoff cannot miss a commit and inactive aggregates retain no wake state.
Event replay owner claims are separate from clustered Session execution ownership. The former already fences synchronized projection reconstruction; the latter still needs distributed active-run acquisition, stale-runtime rejection, interruption, and placement orchestration.
@ -206,7 +210,7 @@ The first V2 `apply_patch` leaf supports add, update, and delete hunks. It parse
- Keep eager structured local-tool settlement: durably record each complete call, start its child execution immediately, await all started settlements after provider-turn consumption, persist every result, and reload history once before continuation.
- Buffer or coalesce streamed deltas before rewriting growing assistant projections.
- Revisit additional covering indexes as larger-history query shapes become concrete.
- Expose replayable Session events over HTTP and the generated SDK where remote consumers need them, deciding whether that public cursor should be opaque rather than the embedded API's branded aggregate sequence.
- Design any global multi-Session event stream separately; the finite history API deliberately reads one authorized Session aggregate and does not change global Event publication.
- Decide whether UI-facing Session subscriptions should optionally interleave ephemeral deltas while connected without advancing the durable cursor.
- Add provider-aware context control for provider-executed tool results. Generic text truncation cannot replace provider-native structured payloads that must round-trip exactly.