fix(agent-loop): second review pass — late-steering discard, dead-branch, catalog leaks

Address a second fresh-eye review of the review fixes:
- MAJOR: late steering that lands after runTurn returns terminally
  stopped (e.g. during the post-turn flush) was drained by runLoop and
  dropped without a discard, leaving a dangling outstanding id the
  negative-only invariant can't catch. Emit agent/inbox/discard for it,
  symmetric with the in-turn terminal-stop drop.
- remove the dead cancel() idle-settle branch: whenIdle's fast path
  already resolves for a lone quiet item, so no waiter is ever left for
  it to settle. Document why.
- gen-cordis-api classShape now drops private/protected/#private members
  and strips getter/setter bodies, so Session no longer leaks private
  fields and getter bodies into the model catalog.
- document that AgentMessage intentionally omits meta (durable-only).

Adds a regression test for the late-steering discard.
This commit is contained in:
Turtle
2026-07-23 22:33:12 +08:00
parent 98ee4ce429
commit 1f9a3e1bee
14 changed files with 122 additions and 69 deletions

View File

@@ -36,20 +36,34 @@ function quote(value: string): string {
* program against, so it belongs in the type closure alongside interfaces.
*/
function classShape(node: ts.ClassDeclaration): ts.ClassDeclaration {
const members = node.members.map((member): ts.ClassElement => {
const isNonPublic = (member: ts.ClassElement): boolean =>
(ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined)?.some(m =>
m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword) ?? false
const members = node.members.flatMap((member): ts.ClassElement[] => {
// A model-facing type shape carries only the public surface — drop private,
// protected, and #private members, and strip every kept member's body.
if (isNonPublic(member) || (ts.isPropertyDeclaration(member) && ts.isPrivateIdentifier(member.name))) return []
if (ts.isMethodDeclaration(member)) {
return ts.factory.updateMethodDeclaration(
return [ts.factory.updateMethodDeclaration(
member, member.modifiers, member.asteriskToken, member.name, member.questionToken,
member.typeParameters, member.parameters, member.type, undefined)
member.typeParameters, member.parameters, member.type, undefined)]
}
if (ts.isConstructorDeclaration(member)) {
return ts.factory.updateConstructorDeclaration(member, member.modifiers, member.parameters, undefined)
return [ts.factory.updateConstructorDeclaration(member, member.modifiers, member.parameters, undefined)]
}
if (ts.isGetAccessorDeclaration(member)) {
return [ts.factory.updateGetAccessorDeclaration(
member, member.modifiers, member.name, member.parameters, member.type, undefined)]
}
if (ts.isSetAccessorDeclaration(member)) {
return [ts.factory.updateSetAccessorDeclaration(
member, member.modifiers, member.name, member.parameters, undefined)]
}
if (ts.isPropertyDeclaration(member)) {
return ts.factory.updatePropertyDeclaration(
member, member.modifiers, member.name, member.questionToken ?? member.exclamationToken, member.type, undefined)
return [ts.factory.updatePropertyDeclaration(
member, member.modifiers, member.name, member.questionToken ?? member.exclamationToken, member.type, undefined)]
}
return member
return [member]
})
return ts.factory.updateClassDeclaration(
node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, members)