feat: expose WIP objects from host namespaces

This commit is contained in:
2026-10-05 15:33:58 +09:00
parent a00a0575f6
commit bc7b2f894c
10 changed files with 914 additions and 188 deletions
+12 -12
View File
@@ -1593,12 +1593,12 @@ where
let engine = worker.engine_mut();
if feature_config.merge_request.any() && wip_mode {
let feature_route = wip_mount_registry
.allocate_feature_route("merge-request")
let namespace_route = wip_mount_registry
.allocate_namespace("merge-request", "merge-requests")
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("allocate Merge Request WIP route: {error}"),
format!("allocate Merge Request WIP namespace: {error}"),
)
})?;
crate::feature::builtin::merge_request::mount_workspace_http_merge_request_wip(
@@ -1606,7 +1606,7 @@ where
workspace_client.clone(),
feature_config.merge_request,
wip_permissions.clone(),
&feature_route,
&namespace_route,
)
.map_err(|error| {
std::io::Error::new(
@@ -1623,12 +1623,12 @@ where
intake: feature_config.ticket.intake,
workflow: feature_config.ticket.workflow,
};
let feature_route = wip_mount_registry
.allocate_feature_route("ticket")
let namespace_route = wip_mount_registry
.allocate_namespace("ticket", "tickets")
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("allocate Ticket WIP route: {error}"),
format!("allocate Ticket WIP namespace: {error}"),
)
})?;
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
@@ -1636,7 +1636,7 @@ where
workspace_client.clone(),
ticket_access,
wip_permissions.clone(),
&feature_route,
&namespace_route,
)
.map_err(|error| {
std::io::Error::new(
@@ -1657,19 +1657,19 @@ where
engine.register_tool(definition);
}
if wip_mode {
let feature_route = wip_mount_registry
.allocate_feature_route("objective")
let namespace_route = wip_mount_registry
.allocate_namespace("objective", "objectives")
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("allocate Objective WIP route: {error}"),
format!("allocate Objective WIP namespace: {error}"),
)
})?;
crate::feature::builtin::objective::mount_workspace_http_objective_wip(
&mut wip_mount_registry,
workspace_client.clone(),
wip_permissions.clone(),
&feature_route,
&namespace_route,
)
.map_err(|error| {
std::io::Error::new(
@@ -5,9 +5,9 @@ use crate::feature::{
};
use crate::permission::permission_action_for;
use crate::wip::{
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipFeatureRoute,
WipMountError, WipMountRegistry, WipOperationError, WipOperationHandler, WipOperationOutput,
WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipMountError,
WipMountRegistry, WipNamespaceRoute, WipOperationError, WipOperationHandler,
WipOperationOutput, WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
};
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolOutput};
@@ -529,7 +529,7 @@ fn native_merge_request_tools(
let (meta, tool) = definition(client.clone(), kind)();
jsonschema::validator_for(&meta.input_schema).map_err(|error| {
WipMountError::InvalidProjection {
route: "/features/merge-request/merge-requests".into(),
route: "/merge-requests".into(),
message: format!("Merge Request tool schema cannot be retained: {error}"),
}
})?;
@@ -562,9 +562,9 @@ pub fn mount_workspace_http_merge_request_wip(
client: Arc<dyn WorkspaceClient>,
config: MergeRequestFeatureConfig,
permissions: Option<ToolPermissionConfig>,
feature_route: &WipFeatureRoute,
namespace_route: &WipNamespaceRoute,
) -> Result<(), WipMountError> {
let collection_route = feature_route.child("merge-requests")?;
let collection_route = namespace_route.root().to_string();
let tools = native_merge_request_tools(client, config)?;
let claimed_tools = tools
.iter()
@@ -1049,7 +1049,7 @@ fn merge_request_operation_declaration(
.get("properties")
.and_then(Value::as_object)
.ok_or_else(|| WipMountError::InvalidProjection {
route: "/features/merge-request/merge-requests".into(),
route: "/merge-requests".into(),
message: format!(
"Merge Request tool `{}` schema has no properties",
tool.name
@@ -1474,7 +1474,9 @@ mod tests {
#[test]
fn native_mount_uses_host_route_and_accepts_only_one_canonical_item_segment() {
let mut registry = WipMountRegistry::new();
let feature_route = registry.allocate_feature_route("merge-request").unwrap();
let namespace_route = registry
.allocate_namespace("merge-request", "merge-requests")
.unwrap();
mount_workspace_http_merge_request_wip(
&mut registry,
Arc::new(TestWorkspaceHttpClient::new("workspace", "http://unused")),
@@ -1484,17 +1486,14 @@ mod tests {
..Default::default()
},
None,
&feature_route,
&namespace_route,
)
.unwrap();
assert_eq!(
registry.routes().collect::<Vec<_>>(),
["/features/merge-request/merge-requests"]
);
assert_eq!(registry.routes().collect::<Vec<_>>(), ["/merge-requests"]);
let resolver = MergeRequestItemResolver {
tools: HashMap::new(),
permissions: None,
collection_route: "/features/merge-request/merge-requests".into(),
collection_route: "/merge-requests".into(),
revisions: Arc::new(Mutex::new(MergeRequestRevisionState::default())),
};
assert!(
@@ -1645,7 +1644,7 @@ mod tests {
let resolver = MergeRequestItemResolver {
tools: HashMap::new(),
permissions: None,
collection_route: "/features/merge-request/merge-requests".into(),
collection_route: "/merge-requests".into(),
revisions: Arc::clone(&revisions),
};
let first = resolver.resolve("MR-1").unwrap().object.validator;
+21 -24
View File
@@ -21,9 +21,9 @@ use wip_protocol::{
use crate::permission::permission_action_for;
use crate::wip::{
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipFeatureRoute,
WipMountError, WipMountRegistry, WipOperationError, WipOperationHandler, WipOperationOutput,
WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipMountError,
WipMountRegistry, WipNamespaceRoute, WipOperationError, WipOperationHandler,
WipOperationOutput, WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
};
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
@@ -551,9 +551,9 @@ pub fn mount_workspace_http_objective_wip(
registry: &mut WipMountRegistry,
client: Arc<dyn WorkspaceClient>,
permissions: Option<ToolPermissionConfig>,
feature_route: &WipFeatureRoute,
namespace_route: &WipNamespaceRoute,
) -> Result<(), WipMountError> {
let collection_route = feature_route.child("objectives")?;
let collection_route = namespace_route.root().to_string();
let backend = WorkspaceHttpObjectiveBackend::new(client);
let revisions = Arc::new(Mutex::new(ObjectiveRevisionState::default()));
let collection_descriptor = objective_collection_descriptor();
@@ -1775,7 +1775,9 @@ mod tests {
}));
let mut registry = WipMountRegistry::new();
let feature_route = registry.allocate_feature_route("objective").unwrap();
let namespace_route = registry
.allocate_namespace("objective", "objectives")
.unwrap();
mount_workspace_http_objective_wip(
&mut registry,
Arc::new(crate::worker::TestWorkspaceHttpClient::new(
@@ -1783,18 +1785,16 @@ mod tests {
"http://backend",
)),
None,
&feature_route,
&namespace_route,
)
.unwrap();
assert_eq!(
registry.routes().collect::<Vec<_>>(),
["/features/objective/objectives"]
assert_eq!(registry.routes().collect::<Vec<_>>(), ["/objectives"]);
assert_eq!(namespace_route.root(), "/objectives");
assert!(
registry
.allocate_namespace("objective", "../tickets")
.is_err()
);
assert_eq!(
feature_route.child("objectives").unwrap(),
"/features/objective/objectives"
);
assert!(feature_route.child("../tickets").is_err());
}
#[test]
@@ -1805,7 +1805,7 @@ mod tests {
crate::worker::TestWorkspaceHttpClient::new("workspace", "http://backend"),
)),
permissions: None,
collection_route: "/features/objective/objectives".into(),
collection_route: "/objectives".into(),
revisions: Arc::clone(&revisions),
};
let initial = resolver.resolve("O-3").unwrap();
@@ -1971,7 +1971,7 @@ mod tests {
let collection = ObjectiveCollectionWipHandler {
backend,
permissions: None,
collection_route: "/features/objective/objectives".into(),
collection_route: "/objectives".into(),
revisions: Arc::new(Mutex::new(ObjectiveRevisionState::default())),
};
let result = collection
@@ -2013,7 +2013,7 @@ mod tests {
let collection = ObjectiveCollectionWipHandler {
backend: backend.clone(),
permissions: None,
collection_route: "/features/objective/objectives".into(),
collection_route: "/objectives".into(),
revisions: Arc::clone(&revisions),
};
let context = || WipCallContext {
@@ -2170,7 +2170,7 @@ mod tests {
crate::worker::TestWorkspaceHttpClient::new("workspace", base_url),
)),
permissions: None,
collection_route: "/features/objective/objectives".into(),
collection_route: "/objectives".into(),
revisions: Arc::new(Mutex::new(ObjectiveRevisionState::default())),
};
let output = match handler
@@ -2191,10 +2191,7 @@ mod tests {
let output = crate::wip::wip_to_json(&output.value).unwrap();
assert_eq!(output["next_cursor"], "next-page");
assert_eq!(output["has_more"], true);
assert_eq!(
output["objectives"][0]["path"],
"/features/objective/objectives/O-3"
);
assert_eq!(output["objectives"][0]["path"], "/objectives/O-3");
assert_eq!(output["objectives"][0]["linked_tickets"][0], "T-7");
assert!(!output.to_string().contains("00001OBJECTIVE"));
assert!(!output.to_string().contains("00001TICKET"));
@@ -2243,7 +2240,7 @@ mod tests {
let resolver = ObjectiveItemResolver {
backend: backend.clone(),
permissions: None,
collection_route: "/features/objective/objectives".into(),
collection_route: "/objectives".into(),
revisions: Arc::clone(&revisions),
};
let before = resolver.resolve("O-3").unwrap().object.validator;
+18 -17
View File
@@ -34,9 +34,9 @@ use crate::feature::{
};
use crate::permission::permission_action_for;
use crate::wip::{
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipFeatureRoute,
WipMountError, WipMountRegistry, WipOperationError, WipOperationHandler, WipOperationOutput,
WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipMountError,
WipMountRegistry, WipNamespaceRoute, WipOperationError, WipOperationHandler,
WipOperationOutput, WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
};
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolOutput};
@@ -1377,7 +1377,7 @@ fn native_ticket_tools(
let (meta, tool) = definition();
let projection = native_ticket_operation(&meta.name).ok_or_else(|| {
WipMountError::InvalidProjection {
route: "/features/ticket/tickets".into(),
route: "/tickets".into(),
message: format!(
"enabled Ticket tool `{}` has no native projection",
meta.name
@@ -1386,7 +1386,7 @@ fn native_ticket_tools(
})?;
jsonschema::validator_for(&meta.input_schema).map_err(|error| {
WipMountError::InvalidProjection {
route: "/features/ticket/tickets".into(),
route: "/tickets".into(),
message: format!("Ticket tool schema cannot be retained: {error}"),
}
})?;
@@ -1418,9 +1418,9 @@ pub fn mount_workspace_http_ticket_wip(
client: Arc<dyn WorkspaceClient>,
access: TicketFeatureAccess,
permissions: Option<ToolPermissionConfig>,
feature_route: &WipFeatureRoute,
namespace_route: &WipNamespaceRoute,
) -> Result<(), WipMountError> {
let collection_route = feature_route.child("tickets")?;
let collection_route = namespace_route.root().to_string();
let tools = native_ticket_tools(client, access)?;
let claimed_tools = tools
.iter()
@@ -1920,7 +1920,7 @@ fn ticket_operation_declaration(
.get("properties")
.and_then(Value::as_object)
.ok_or_else(|| WipMountError::InvalidProjection {
route: "/features/ticket/tickets".into(),
route: "/tickets".into(),
message: format!("Ticket tool `{}` schema has no properties", tool.name),
})?;
let required = tool
@@ -2103,7 +2103,7 @@ mod tests {
#[test]
fn native_ticket_mount_uses_host_route_and_rejects_non_ticket_children() {
let mut registry = WipMountRegistry::new();
let feature_route = registry.allocate_feature_route("ticket").unwrap();
let namespace_route = registry.allocate_namespace("ticket", "tickets").unwrap();
mount_workspace_http_ticket_wip(
&mut registry,
Arc::new(crate::worker::TestWorkspaceHttpClient::new(
@@ -2112,20 +2112,21 @@ mod tests {
)),
TicketFeatureAccess::review(),
None,
&feature_route,
&namespace_route,
)
.unwrap();
assert_eq!(
registry.routes().collect::<Vec<_>>(),
["/features/ticket/tickets"]
assert_eq!(registry.routes().collect::<Vec<_>>(), ["/tickets"]);
assert!(
registry
.allocate_namespace("ticket", "../objectives")
.is_err()
);
assert!(feature_route.child("../objectives").is_err());
let revisions = Arc::new(Mutex::new(TicketRevisionState::default()));
let resolver = TicketItemResolver {
tools: HashMap::new(),
permissions: None,
collection_route: "/features/ticket/tickets".into(),
collection_route: "/tickets".into(),
revisions,
};
assert!(resolver.resolve("T-42").is_some());
@@ -2189,7 +2190,7 @@ mod tests {
let resolver = TicketItemResolver {
tools: HashMap::new(),
permissions: None,
collection_route: "/features/ticket/tickets".into(),
collection_route: "/tickets".into(),
revisions: Arc::clone(&revisions),
};
let unobserved_internal = resolver.resolve("00001TICKET").unwrap().object.validator;
@@ -2243,7 +2244,7 @@ mod tests {
let resolver = TicketItemResolver {
tools: HashMap::new(),
permissions: None,
collection_route: "/features/ticket/tickets".into(),
collection_route: "/tickets".into(),
revisions: Arc::clone(&revisions),
};
let target_v1 = resolver.resolve("T-43").unwrap().object.validator;
+809 -105
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -35,6 +35,8 @@ It is not a dumping ground for external research, old plans, API inventories, or
27. [`development/subjektiv-product-cutover.md`](development/subjektiv-product-cutover.md) — legacy Workspace Memoryからsubjektivへのoperator cutover、explicit reset、backup/rollback runbook。
28. [`design/worker-wip-mode.md`](design/worker-wip-mode.md) — explicit-opt-in Worker WIP mode, Worldspace projection, authority, cache, and retry contracts.
29. [`design/objective-wip-projection.md`](design/objective-wip-projection.md) — Host-assigned Objective collection/item routes, operation mapping, Backend authority, bounds, and compatibility suppression.
30. [`design/ticket-wip-projection.md`](design/ticket-wip-projection.md) — Host-assigned Ticket collection/item routes, role-specific operations, route identity, and typed Backend preservation.
31. [`design/merge-request-wip-projection.md`](design/merge-request-wip-projection.md) — Host-assigned Merge Request routes and preserved review/integration authority gates.
## What belongs here
+5 -3
View File
@@ -4,13 +4,15 @@
When a Worker explicitly runs in `wip` mode and its Merge Request Feature has at least one enabled operation, the Worker Host projects that exact operation set as native Worldspace objects. The projection is an adapter over the existing scoped Workspace Merge Request tools and Backend routes. It does not create a second Merge Request store, provider integration path, review grant, approval rule, or completion authority.
The Host allocates `/features/merge-request` and the Feature mounts:
The Host assigns the Merge Request provider the root `merge-requests` namespace through `WipMountRegistry` and mounts:
```text
/features/merge-request/merge-requests
/features/merge-request/merge-requests/<merge-request-id>
/merge-requests
/merge-requests/<merge-request-id>
```
There is no Feature Object or `/features/merge-request` compatibility alias. Legacy paths remain historical display data only and fail as current input with rediscovery guidance.
An item identity is a single canonical path segment. Nested paths and percent-encoded target substitution do not resolve. Item operations remove `merge_request_id` from their descriptor and reconstruct it only from the selected object route. Discovery does not mint authority: every call still uses the Worker's scoped `WorkspaceClient`, the enabled Feature flags, ordinary Tool permission rules, and the Backend's Workspace, repository, assignment, provider-ref, review, and integration checks.
## Current Feature inventory
+5 -3
View File
@@ -4,13 +4,15 @@
When a Worker explicitly runs in `wip` mode and the Objective Feature is enabled, the Worker Host projects Objectives as native Worldspace objects. The projection is an adapter over the existing scoped Workspace API. It does not introduce another Objective store, state machine, reference resolver, or audit path.
The Host allocates `/features/objective` to the Feature through `WipMountRegistry`. The Objective Feature then mounts:
The Host assigns the Objective provider the root `objectives` namespace through `WipMountRegistry` and mounts:
```text
/features/objective/objectives
/features/objective/objectives/<objective-reference>
/objectives
/objectives/<objective-reference>
```
There is no Feature Object or `/features/objective` compatibility alias. Legacy paths are retained only as historical text and fail as current inputs with rediscovery guidance.
`<objective-reference>` is either a canonical `O-<digits>` key or an existing canonical internal alphanumeric ID. Only one direct child segment is accepted. Nested suffixes, Ticket keys, and unrelated or hidden prefixes do not resolve as Objective objects.
The Backend remains authoritative for existence, `O-*`/internal-ID resolution, Workspace scope, validation, audit events, revision generation, pagination, body/event bounds, and linked-Ticket validation. A discovered route is not independent authority; every operation still uses the Worker's current `WorkspaceClient`.
+5 -3
View File
@@ -4,13 +4,15 @@
When a Worker explicitly runs in `wip` mode and its Ticket Feature is enabled, the Worker Host projects the enabled Ticket surface as native Worldspace objects. The projection is an adapter over the existing scoped Workspace Ticket API and typed Ticket tools. It does not add a Ticket store, resolver, audit path, state machine, readiness rule, queue path, or relation implementation.
The Host allocates `/features/ticket` and the Feature mounts:
The Host assigns the Ticket provider the root `tickets` namespace through `WipMountRegistry` and mounts:
```text
/features/ticket/tickets
/features/ticket/tickets/<ticket-reference>
/tickets
/tickets/<ticket-reference>
```
There is no Feature Object or `/features/ticket` compatibility alias. A legacy path supplied as current input fails with rediscovery guidance; saved history containing that text is left unchanged.
`<ticket-reference>` is either a canonical `T-<digits>` key or an existing canonical internal alphanumeric ID. Only a direct child is accepted. Nested suffixes, Objective keys, unrelated prefixes, and hidden routes do not resolve.
Every call retains the Worker's scoped `WorkspaceClient`, current Ticket Feature access, and ordinary Tool permission policy. Discovery is not mutation authority. A direct call to an unpublished operation fails even when the caller knows its name. `deny` and interactive `ask` permission outcomes fail closed before Backend dispatch.
+23 -6
View File
@@ -52,21 +52,36 @@ The interface descriptor uses WIP `Json` for compatibility input and includes th
## Native projection extension and collisions
`worker::wip::WipMountRegistry` is the Host-owned mount allocator. A Feature can independently construct a `WipProjection` with an async `WipOperationHandler`, then pass its aggregate registry through `install_wip_mode_with_mounts` before compatibility finalization. A Feature first requests a Host-owned `/features/<feature>` namespace with `allocate_feature_route`; checked direct-child joins prevent a Feature from claiming an ambient global route.
`worker::wip::WipMountRegistry` is the Host-owned namespace, Object, and operation registry. A provider requests a root namespace with `allocate_namespace(owner, namespace)`; the owner is retained as Host metadata and is not inserted into the public path. It then mounts the collection Object directly at `/<namespace>` and may install a bounded direct-child resolver. Features do not appear as intermediate Objects.
- Routes must be canonical absolute WIP paths.
Object resolution and operations are separate contracts. One mounted `WipProjection` owns a static Object route and its initial operations. Another Feature may add operations with `contribute_operations` without replacing that Object. Dynamic families likewise keep one `WipDynamicItemResolver`, while `contribute_dynamic_operations` supplies disjoint handlers for every resolved item without a second resolver. The Host merges only contributions with the same interface format, documentation, and type declarations; duplicate operation names, inconsistent interfaces/validators, missing targets, duplicate namespace owners, and unrelated route claims fail deterministically at registration. The merged interface validator changes with the effective operation set, so stale Known Space cannot authorize a newly contributed operation.
- Routes must be canonical absolute WIP paths, and allocated namespaces are one lowercase root segment.
- Object names must match the final route segment.
- A projected object exposes exactly its declared interface.
- Unrelated duplicate routes and conflicting interface descriptors or validators fail startup.
- Object existence and resolution have one owner; operation contributors cannot remount or substitute the Object.
- Multiple Features may contribute disjoint operations to the same Object interface.
- Unrelated duplicate routes, duplicate operation names, and conflicting interface shapes fail startup.
- A bounded dynamic mount may resolve exactly one item segment below an already mounted native collection; deeper paths never fall through to the resolver.
- A native projection replaces a compatibility projection either by the original same-route semantic capability or by an explicit registry capability claim owned by a mounted native route.
- Conflicting native claims fail startup. When native is selected, the claimed compatibility tool is hidden rather than exposed through a second entry. Normal Tool mode is unaffected because it does not install the WIP registry.
`WipInspect` renders the complete descriptor, including descriptor-local named declarations and recursive record, list, enum, and union shapes with required flags and declaration documentation. The unit samples `mount_collision_and_native_replacement_are_explicit` and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate independent native registration, deterministic replacement, collision rejection, complete descriptor inspection, and a descriptor-typed native invocation.
`WipInspect` renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation.
The built-in native route migration is:
| Historical route | Canonical route |
| --- | --- |
| `/features/ticket/tickets[/<reference>]` | `/tickets[/<reference>]` |
| `/features/objective/objectives[/<reference>]` | `/objectives[/<reference>]` |
| `/features/merge-request/merge-requests[/<reference>]` | `/merge-requests[/<reference>]` |
`/repositories` and `/workdirs` are reserved for a later provider using the same registration contract; this layer does not implement those domains. `/checkouts` content projection is likewise outside this contract.
## Authority and execution
Discovery is not authority. The Worldspace contains only the tools already registered from enabled Features and the Worker's current execution context. Calling a compatibility operation:
Discovery is not authority. Object publication, operation implementation, target capability, and subject permission are independent checks. An Object may remain discoverable through an enabled read provider when a management Feature contributes no operations; conversely, publishing an operation never bypasses the target's current capability or the subject's current permission. Every handler re-evaluates permission at call time, so revocation after discovery is rejected before provider dispatch. Feature enablement selects implementations and does not itself grant read or mutation authority.
The Worldspace contains only the operations registered from enabled Features and the Worker's current execution context. Calling a compatibility operation:
1. resolves the exact mounted route;
2. verifies object and interface membership and validators;
@@ -85,6 +100,8 @@ The stateful `wip-client::Client` is owned by one Worker WIP runtime. Sessions a
Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `WipDiscover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority.
Saved Session/history entries remain append-only evidence and are never rewritten or replayed as calls. In particular, historical `/features/<feature>/...` text stays displayable but is not a current route alias. Passing such a legacy path to discovery or call returns `NotFound` with guidance to rediscover from `/`; the Host does not translate it, resolve it under another namespace, or expose old and new routes in parallel.
## Errors, cancellation, and retries
The adapter keeps these outcomes distinct: