fix: a delegated session inherits the facet it was spawned under
Release / release (push) Successful in 6s
Release / release (push) Successful in 6s
The user's declaration is keyed by session id, and a session spawned or forked from a scoped one has none of its own — so its facet was its own working directory, one level above its parent's. Delegation was therefore a way around the scope rather than work inside it: the parent could not read the file itself and could have a subagent read it. A session now inherits the root resolved for the session named by its `parentSession` header, which the harness already records durably for both a spawned child and a fork. The inherited root is the parent's *effective* root, so it already includes any narrowing the parent made for itself: delegating is not a way back out of a self-imposed focus either. Because each session records its own root as it resolves a scope, and a scope is resolved for every model request and every tool call, inheritance holds at any depth of delegation rather than for one hop. Two boundaries are deliberate. A declaration the user made *for the child* is a more specific statement about that session and stands — inheritance only answers the case where they made none. And a parent's recorded root outlives its session while any child that inherited it is still running, because disposing a parent must not quietly widen a subagent that is still working.
This commit is contained in:
1 parent
a9a25490db
commit
8ab3299694
4 files changed
+245
-11
No files matched your search
@@ -187,3 +187,61 @@ test('junk in a declaration is ignored rather than thrown on', () => {
|
||||
assert.deepEqual(policy.scopeFor(agentAt('/w', 's1')).extras, ['/w/ok'])
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's2')).root, '/w')
|
||||
})
|
||||
|
||||
// ── delegated sessions ───────────────────────────────────────────────────────
|
||||
//
|
||||
// A session spawned or forked from this one has no declaration of its own, and
|
||||
// the user's declaration is keyed by session id. What stops it starting wider
|
||||
// than its parent is the parent's own resolved root, recorded as the parent
|
||||
// resolves a scope — which it does for every model request and every tool call,
|
||||
// so the record is always there before it can spawn anything.
|
||||
|
||||
/** An agent whose header carries the lineage the harness writes for a child. */
|
||||
const spawned = (id, parentSession, cwd = '/w') => ({
|
||||
session: { id, header: { cwd, parentSession } },
|
||||
})
|
||||
|
||||
test('a session spawned from a scoped one inherits its root, at any depth', () => {
|
||||
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src/room' } } })
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
|
||||
|
||||
const child = spawned('c1', 's1')
|
||||
assert.equal(policy.scopeFor(child).root, '/w/src/room')
|
||||
// Relative paths still resolve from the child's own directory, as for anyone.
|
||||
assert.equal(policy.scopeFor(child).cwd, '/w')
|
||||
assert.equal(policy.scopeFor(spawned('g1', 'c1')).root, '/w/src/room')
|
||||
})
|
||||
|
||||
test('a child is never wider than its parent is now, narrowing included', () => {
|
||||
const policy = createFacetPolicy({ mode: 'deny' })
|
||||
policy.narrowTo('s1', '/w/src/room')
|
||||
|
||||
// The narrowing is the agent's own decision; spawning a child is not a way to
|
||||
// undo it, because the root the child inherits already includes it.
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
|
||||
assert.equal(policy.scopeFor(spawned('c1', 's1')).root, '/w/src/room')
|
||||
})
|
||||
|
||||
test('a declaration for the child itself stands over what it would inherit', () => {
|
||||
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src/room' }, c1: { focus: '.' } } })
|
||||
policy.scopeFor(agentAt('/w', 's1'))
|
||||
|
||||
// Inheritance answers the case where the user made no statement about this
|
||||
// session; a statement they did make is the more specific one and stands.
|
||||
assert.equal(policy.scopeFor(spawned('c1', 's1')).root, '/w')
|
||||
})
|
||||
|
||||
test('a root is kept while a session that inherited it is still alive', () => {
|
||||
const policy = createFacetPolicy({ mode: 'deny', scopes: { s1: { focus: 'src/room' } } })
|
||||
const child = spawned('c1', 's1')
|
||||
policy.scopeFor(agentAt('/w', 's1'))
|
||||
policy.scopeFor(child)
|
||||
|
||||
// Disposing the parent must not quietly widen the child that is still running.
|
||||
policy.forget('s1')
|
||||
assert.equal(policy.scopeFor(child).root, '/w/src/room')
|
||||
|
||||
// With nothing left to inherit it, the record goes too.
|
||||
policy.forget('c1')
|
||||
assert.equal(policy.scopeFor(agentAt('/w', 's1')).root, '/w/src/room')
|
||||
})
|
||||
Reference in new issue
Block a user