feat(session-name): classifier virtual-domain 4th leg [M1 A2]

M1 gateway A2. Contract 2cf541c6 / arch verdict 8cd30094 (both verified on-box).
Adds a CLOSED virtual-domain leg (<local>@external) to the parity-pinned FR-8
session-name contract — a LEXICAL widening only; ADMISSION stays at the A1/A4 gateway.

- kind 'external' {local, domain}; VIRTUAL_DOMAIN_TOKENS = ["external"] as a CLOSED
  NAMED CONSTANT beside a derived pattern (arch: not inline alternation; pure fn, no IO).
- isHumanSeatSessionRef WIDENED = HUMAN_SEAT || VIRTUAL_DOMAIN: @external refs are
  human-CLASS (they JOIN the attention/human-route union — NEVER a silent agent-class
  downgrade). parseSessionName precedence human -> external -> canonical -> legacy -> malformed.
- Edited daemon session-name.ts FIRST -> mirrored VERBATIM to cli + ui (cli≡ui byte-identical).
- Parity test: +7 vectors (mike@external, scheme slack:U...@external, dotted/hyphenated,
  decided human@external POSITIVE; @external + mike@externalx NEG) + agreement-pin RHS
  updated to PIN the intentional widening (not silent).
- 5-consumer consolidation onto the ONE predicate: switched routes/queue.ts isAttentionItem
  from human-route-enforcer's isHumanSeatSession to isHumanSeatSessionRef (behavior-identical
  today, gains @external). Sweep clean: queue.ts + queue-bridge + startup + attention-feed +
  feed-classifier all on isHumanSeatSessionRef; no hidden re-impl.

DOCUMENTED DIVERGENCE (route as follow-on, per dev-planner): human-route-enforcer.ts stays
READ-ONLY this slice, so its other consumers (mission-control-write-contract, queue-repository
park-validation, review/gather) keep the narrow @(kernel|host) pattern — @external is not
human-class THERE yet. Reserved-token 'external' rail is A1's build (proof-7 verifies post-A1).

Parity 66/66; attention-route + queue-route targeted 54/54; tsc daemon/cli/ui 0. Based on
local main c4d5f4e3 (origin/main was 12 folds stale — never-push means local main is truth).
Never push.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Opus 4.8 committed 2026-08-06 17:37:47 +00:00
1 parent c4d5f4e3e3
commit 3e688f6347
6 files changed
+153 -22

No files matched your search

+27 -3
View File
@@ -33,18 +33,42 @@
export type ParsedSessionName =
| { kind: "canonical"; member: string; rig: string }
| { kind: "legacy"; name: string }
// A2 4th leg — virtual-domain ref (<local>@external). `local` is verbatim up to
// the domain "@" (the scheme leg, slack:U..., rides `local` LEXICALLY; the
// registered-vs-scheme split is a GATEWAY parse, never the classifier's).
| { kind: "external"; local: string; domain: string }
| { kind: "malformed"; error: "malformed_session_name"; input: string };
const HUMAN_SEAT_SESSION_REF_PATTERN = /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/;
const LEGACY_FLAT_SESSION_PATTERN = /^r\d{2}-.+$/;
// A2 4th leg — CLOSED virtual-domain token set as a NAMED CONSTANT beside the
// pattern (arch: not inline alternation; lexical-only pure fn, no state/IO).
// Future domain words extend this array (e.g. @a2a) as explicit alternation.
// Admission (is this @external ref registered?) is NOT here — it is the A1/A4
// gateway's job; the classifier only recognizes the lexical shape.
const VIRTUAL_DOMAIN_TOKENS = ["external"] as const;
const VIRTUAL_DOMAIN_SESSION_REF_PATTERN = new RegExp(
`^[A-Za-z0-9._:-]+@(${VIRTUAL_DOMAIN_TOKENS.join("|")})$`
);
/** Arch tooth 1: human-seat classification runs BEFORE any parse at sites
* where the human/agent distinction matters (the queue-destination gate). */
/** Arch tooth 1: human-CLASS classification runs BEFORE any parse at sites where
* the human/agent distinction matters (the queue-destination gate + attention
* union). A2 widened it: virtual-domain refs (<local>@external) are human-CLASS
* too (they join human-route enforcement; NEVER a silent agent-class downgrade).
* Admission stays at the A1/A4 gateway. */
export function isHumanSeatSessionRef(sessionRef: string): boolean {
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef);
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef) ||
VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(sessionRef);
}
export function parseSessionName(raw: string): ParsedSessionName {
// Precedence (arch): human -> EXTERNAL -> canonical -> legacy -> malformed. The
// virtual-domain leg MUST precede canonical, else "mike@external" parses as
// member=mike rig=external; A1's rig-name reservation keeps that unambiguous.
if (VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(raw)) {
const domainAt = raw.lastIndexOf("@");
return { kind: "external", local: raw.slice(0, domainAt), domain: raw.slice(domainAt + 1) };
}
const at = raw.indexOf("@");
if (at > 0 && at < raw.length - 1) {
return { kind: "canonical", member: raw.slice(0, at), rig: raw.slice(at + 1) };
+27 -3
View File
@@ -132,18 +132,42 @@ export function validateSessionComponents(
export type ParsedSessionName =
| { kind: "canonical"; member: string; rig: string }
| { kind: "legacy"; name: string }
// A2 4th leg — virtual-domain ref (<local>@external). `local` is verbatim up to
// the domain "@" (the scheme leg, slack:U..., rides `local` LEXICALLY; the
// registered-vs-scheme split is a GATEWAY parse, never the classifier's).
| { kind: "external"; local: string; domain: string }
| { kind: "malformed"; error: "malformed_session_name"; input: string };
const HUMAN_SEAT_SESSION_REF_PATTERN = /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/;
const LEGACY_FLAT_SESSION_PATTERN = /^r\d{2}-.+$/;
// A2 4th leg — CLOSED virtual-domain token set as a NAMED CONSTANT beside the
// pattern (arch: not inline alternation; lexical-only pure fn, no state/IO).
// Future domain words extend this array (e.g. @a2a) as explicit alternation.
// Admission (is this @external ref registered?) is NOT here — it is the A1/A4
// gateway's job; the classifier only recognizes the lexical shape.
const VIRTUAL_DOMAIN_TOKENS = ["external"] as const;
const VIRTUAL_DOMAIN_SESSION_REF_PATTERN = new RegExp(
`^[A-Za-z0-9._:-]+@(${VIRTUAL_DOMAIN_TOKENS.join("|")})$`
);
/** Arch tooth 1: human-seat classification runs BEFORE any parse at sites
* where the human/agent distinction matters (the queue-destination gate). */
/** Arch tooth 1: human-CLASS classification runs BEFORE any parse at sites where
* the human/agent distinction matters (the queue-destination gate + attention
* union). A2 widened it: virtual-domain refs (<local>@external) are human-CLASS
* too (they join human-route enforcement; NEVER a silent agent-class downgrade).
* Admission stays at the A1/A4 gateway. */
export function isHumanSeatSessionRef(sessionRef: string): boolean {
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef);
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef) ||
VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(sessionRef);
}
export function parseSessionName(raw: string): ParsedSessionName {
// Precedence (arch): human -> EXTERNAL -> canonical -> legacy -> malformed. The
// virtual-domain leg MUST precede canonical, else "mike@external" parses as
// member=mike rig=external; A1's rig-name reservation keeps that unambiguous.
if (VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(raw)) {
const domainAt = raw.lastIndexOf("@");
return { kind: "external", local: raw.slice(0, domainAt), domain: raw.slice(domainAt + 1) };
}
const at = raw.indexOf("@");
if (at > 0 && at < raw.length - 1) {
return { kind: "canonical", member: raw.slice(0, at), rig: raw.slice(at + 1) };
+11 -6
View File
@@ -8,8 +8,7 @@ import type {
} from "../domain/queue-repository.js";
import { QueueRepositoryError, newQitemId, deriveCrossHostSuccessorId, stampSelfHostSuffix } from "../domain/queue-repository.js";
import type { QueueItem } from "../domain/queue-repository.js";
import { isHumanSeatSession } from "../domain/human-route-enforcer.js";
import { parseSessionName } from "../domain/session-name.js";
import { parseSessionName, isHumanSeatSessionRef } from "../domain/session-name.js";
import { hostname as osHostname } from "node:os";
import type { InboxHandler } from "../domain/inbox-handler.js";
import { InboxHandlerError } from "../domain/inbox-handler.js";
@@ -72,8 +71,13 @@ export function crossHostProvenanceTags(existing: string[] | undefined): string[
*/
export function isAttentionItem(q: { tier: string | null; destinationSession: string; state?: string; blockedOn?: string | null }): boolean {
if (q.tier === "human-gate") return true;
if (isHumanSeatSession(q.destinationSession)) return true;
return q.state === "blocked" && isHumanSeatSession(q.blockedOn ?? null);
// A2: consolidated onto the ONE contract predicate (isHumanSeatSessionRef) so
// virtual-domain refs (<local>@external) are human-CLASS and JOIN the attention
// union — behavior-identical for every non-@external ref (same pattern today).
// The enforcer's other consumers keep the narrow pattern this slice (a DOCUMENTED
// divergence, routed as a follow-on per dev-planner's ruling).
if (isHumanSeatSessionRef(q.destinationSession)) return true;
return q.state === "blocked" && isHumanSeatSessionRef(q.blockedOn ?? "");
}
@@ -717,8 +721,9 @@ export function queueRoutes(): Hono {
// source of truth for the UI Action-required + Approval lenses) so
// those surfaces don't depend on the lossy ephemeral client event
// FIFO. Class membership matches the mission-control read layer
// semantics: tier='human-gate' OR destination matches
// /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/. Open state defaults
// semantics: tier='human-gate' OR the ONE contract predicate
// isHumanSeatSessionRef (human-seat /^human…@(kernel|host)$/ OR the A2
// virtual-domain leg <local>@external). Open state defaults
// to pending|in-progress|blocked (callers can still override via
// `state=...`). Composable with destinationSession/sourceSession/
// targetRepo/limit.
@@ -127,6 +127,52 @@ const SHARED_VECTORS = [
parsed: malformed("r3-worker"),
human: false, memberLabel: "r3-worker", rigOf: undefined,
},
// ── A2 4th leg: virtual-domain (@external). Contract 2cf541c6 / verdict 8cd30094.
// kind 'external' {local, domain}; the human-CLASS predicate returns TRUE (no silent
// agent-class downgrade); rigOf undefined (a virtual domain is NOT a routing rig).
// Admission (registered vs scheme, refuse-if-unregistered) is the A1/A4 GATEWAY's job.
{
label: "4TH LEG: registered virtual-domain ref mike@external",
input: "mike@external",
parsed: { kind: "external", local: "mike", domain: "external" },
human: true, memberLabel: "mike", rigOf: undefined,
},
{
label: "4TH LEG: scheme-form local rides the leg LEXICALLY (slack:U012AB3CD@external)",
input: "slack:U012AB3CD@external",
parsed: { kind: "external", local: "slack:U012AB3CD", domain: "external" },
human: true, memberLabel: "slack:U012AB3CD", rigOf: undefined,
},
{
label: "4TH LEG: dotted local (my.name@external)",
input: "my.name@external",
parsed: { kind: "external", local: "my.name", domain: "external" },
human: true, memberLabel: "my.name", rigOf: undefined,
},
{
label: "4TH LEG: hyphenated local (my-name@external)",
input: "my-name@external",
parsed: { kind: "external", local: "my-name", domain: "external" },
human: true, memberLabel: "my-name", rigOf: undefined,
},
{
label: "4TH LEG decided human@external: local 'human' is valid -> external (human-class), refused-at-gateway if unregistered",
input: "human@external",
parsed: { kind: "external", local: "human", domain: "external" },
human: true, memberLabel: "human", rigOf: undefined,
},
{
label: "4TH LEG neg: empty local (@external) -> malformed (not agent-downgraded)",
input: "@external",
parsed: malformed("@external"),
human: false, memberLabel: "", rigOf: undefined,
},
{
label: "4TH LEG neg: domain not in the closed set (mike@externalx) -> canonical, NOT external",
input: "mike@externalx",
parsed: { kind: "canonical", member: "mike", rig: "externalx" },
human: false, memberLabel: "mike", rigOf: "externalx",
},
] as const;
describe("session-name contract — three-copy parity (shared vectors)", () => {
@@ -149,12 +195,18 @@ describe("session-name contract — three-copy parity (shared vectors)", () => {
expect(cliSrc).toBe(uiSrc);
});
it("the contract's human-seat predicate agrees with the read-only human-route-enforcer pattern on every vector", () => {
// human-route-enforcer.ts is READ-ONLY this slice (PRD constraint) and
// keeps its own pattern; this read-only import pins the contract copy
// against it so neither can drift silently.
it("the contract's human-CLASS predicate = the read-only enforcer human-seat pattern OR the A2 virtual-domain leg (intentional widening, pinned not silent)", () => {
// human-route-enforcer.ts is READ-ONLY this slice (PRD constraint) and keeps
// its narrow human-seat pattern. A2 widened the contract predicate to also
// admit virtual-domain refs (<local>@external) as human-CLASS. That divergence
// is INTENTIONAL, so we pin the exact union — enforcer-human-seat OR the
// contract's own external kind — so neither side can drift SILENTLY. (The
// enforcer's remaining consumers keep the narrow pattern this slice: a
// DOCUMENTED divergence routed as a follow-on, per dev-planner's ruling.)
for (const v of SHARED_VECTORS) {
expect(daemonCopy.isHumanSeatSessionRef(v.input)).toBe(HUMAN_SEAT_SESSION_PATTERN.test(v.input));
const enforcerHumanSeat = HUMAN_SEAT_SESSION_PATTERN.test(v.input);
const virtualDomainLeg = daemonCopy.parseSessionName(v.input).kind === "external";
expect(daemonCopy.isHumanSeatSessionRef(v.input)).toBe(enforcerHumanSeat || virtualDomainLeg);
}
});
});
+4 -2
View File
@@ -81,8 +81,10 @@ async function fetchAggregatedAttention(limit?: number): Promise<AttentionData>
}
/**
* Open attention-class qitems (tier="human-gate" OR destination
* matches /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/). State default
* Open attention-class qitems (tier="human-gate" OR destination is
* human-CLASS via isHumanSeatSessionRef — human-seat
* /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/ OR the A2 virtual-domain
* leg <local>@external). State default
* is pending|in-progress|blocked — closed/done items are NOT surfaced.
*
* `limit` (optional, default 50) caps the rendered set so a
+27 -3
View File
@@ -33,18 +33,42 @@
export type ParsedSessionName =
| { kind: "canonical"; member: string; rig: string }
| { kind: "legacy"; name: string }
// A2 4th leg — virtual-domain ref (<local>@external). `local` is verbatim up to
// the domain "@" (the scheme leg, slack:U..., rides `local` LEXICALLY; the
// registered-vs-scheme split is a GATEWAY parse, never the classifier's).
| { kind: "external"; local: string; domain: string }
| { kind: "malformed"; error: "malformed_session_name"; input: string };
const HUMAN_SEAT_SESSION_REF_PATTERN = /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/;
const LEGACY_FLAT_SESSION_PATTERN = /^r\d{2}-.+$/;
// A2 4th leg — CLOSED virtual-domain token set as a NAMED CONSTANT beside the
// pattern (arch: not inline alternation; lexical-only pure fn, no state/IO).
// Future domain words extend this array (e.g. @a2a) as explicit alternation.
// Admission (is this @external ref registered?) is NOT here — it is the A1/A4
// gateway's job; the classifier only recognizes the lexical shape.
const VIRTUAL_DOMAIN_TOKENS = ["external"] as const;
const VIRTUAL_DOMAIN_SESSION_REF_PATTERN = new RegExp(
`^[A-Za-z0-9._:-]+@(${VIRTUAL_DOMAIN_TOKENS.join("|")})$`
);
/** Arch tooth 1: human-seat classification runs BEFORE any parse at sites
* where the human/agent distinction matters (the queue-destination gate). */
/** Arch tooth 1: human-CLASS classification runs BEFORE any parse at sites where
* the human/agent distinction matters (the queue-destination gate + attention
* union). A2 widened it: virtual-domain refs (<local>@external) are human-CLASS
* too (they join human-route enforcement; NEVER a silent agent-class downgrade).
* Admission stays at the A1/A4 gateway. */
export function isHumanSeatSessionRef(sessionRef: string): boolean {
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef);
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef) ||
VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(sessionRef);
}
export function parseSessionName(raw: string): ParsedSessionName {
// Precedence (arch): human -> EXTERNAL -> canonical -> legacy -> malformed. The
// virtual-domain leg MUST precede canonical, else "mike@external" parses as
// member=mike rig=external; A1's rig-name reservation keeps that unambiguous.
if (VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(raw)) {
const domainAt = raw.lastIndexOf("@");
return { kind: "external", local: raw.slice(0, domainAt), domain: raw.slice(domainAt + 1) };
}
const at = raw.indexOf("@");
if (at > 0 && at < raw.length - 1) {
return { kind: "canonical", member: raw.slice(0, at), rig: raw.slice(at + 1) };