fix(invariants): harden runtime contracts and gates

This commit is contained in:
Tianyi Cui
2026-07-21 00:25:38 +08:00
parent a4be4b5e52
commit 0c08e34a4a
27 changed files with 288 additions and 111 deletions

View File

@@ -40,7 +40,7 @@ The current executable companions protect these relationships:
| `dsh-fs`, `dsh-subagent`, `dsh-workflow` | Filesystem event identity, provider/child pairing, and workflow/agent lifecycle identity. |
| `dsh-permission`, `dsh-user-approval` | Active-preset references and approval asked/decided audit pairing. |
| `dsh-tasks`, `dsh-tool-todo` | Task snapshot lifecycle/ownership fields and durable whole-list todo structure. |
| `dsh-time-context` | Durable clock readings agree with the session's open turn and next pre-step position, elapsed baseline, and event timestamp. |
| `dsh-time-context` | Durable clock readings agree with the session's open turn and next pre-step position and elapsed baseline; rendered time parses and does not postdate its event. |
The root entrypoint of each owner remains independent of diagnostics. Loading the service alone installs no product checks, and loading a companion without the service waits on its declared `invariants` injection.
@@ -77,6 +77,6 @@ None; invariant checks do not assemble or send provider requests.
## Known Limitations and Deferred Work
- Request reconstruction covers frozen loop-built requests with a live session id; direct one-shot LLM calls remain outside that marker contract.
- Request reconstruction covers requests explicitly marked by the loop before freezing; direct one-shot LLM calls remain outside that marker contract even when callers freeze them or attach a session id.
- Live-only lifecycle companions cannot reconstruct operations that began before their own reload. Standard and test compositions mount them before the corresponding operations begin.
- Regular-expression filters are fixed for the service lifetime; changing them requires ordinary Cordis plugin reload.

View File

@@ -162,27 +162,28 @@ export class InvariantService extends Service {
throw new InvariantError(packageName, message)
})
)
const child = ctx.plugin(installer.inject === undefined
? installInvariant
: Object.assign(installInvariant, { inject: installer.inject }))
try {
await child
} catch (error) {
try {
await child.dispose()
} finally {
registrations.delete(packageName)
}
throw error
}
const child = ctx.plugin(installer.inject === undefined
? installInvariant
: Object.assign(installInvariant, { inject: installer.inject }))
return async () => {
try {
await child
} catch (error) {
await child.dispose()
} finally {
registrations.delete(packageName)
throw error
}
return async () => {
try {
await child.dispose()
} finally {
registrations.delete(packageName)
}
}
} catch (error) {
registrations.delete(packageName)
throw error
}
}, `invariants.register(${JSON.stringify(packageName)})`)
} catch (error) {

View File

@@ -260,6 +260,28 @@ describe('InvariantService lifecycle', () => {
expect(retry).toHaveBeenCalledOnce()
})
it('rolls back publication effects and ownership when child-fiber publication fails', async () => {
const { ctx } = await setup()
const leaked = vi.fn()
let rejectPublication = true
const stopRejecting = ctx.on('internal/plugin', (fiber) => {
if (!rejectPublication || fiber.uid === null) return
rejectPublication = false
fiber.ctx.on('invariants-test/ping', leaked, { global: true })
throw new Error('publication failed')
})
const failed = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-publication-probe', () => {}))
await expect(Promise.resolve(failed)).rejects.toThrow('publication failed')
ctx.emit('invariants-test/ping')
expect(leaked).not.toHaveBeenCalled()
stopRejecting()
const retry = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-publication-probe', () => {}))
await retry
await retry()
})
it('joins asynchronous checks and rolls back their effects on failure', async () => {
const { ctx } = await setup()
const leaked = vi.fn()