diff --git a/apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts b/apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts index 7bb1118..fc8b666 100644 --- a/apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts +++ b/apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts @@ -7,7 +7,7 @@ import { } from '@mosaicstack/discord-plugin'; import { InteractionController } from '../../agent/interaction.controller.js'; import { RuntimeProviderService } from '../../agent/runtime-provider-registry.service.js'; -import { TessDurableSessionService } from '../../agent/tess-durable-session.service.js'; +import { DurableSessionService } from '../../agent/durable-session.service.js'; import { ChatGateway } from '../../chat/chat.gateway.js'; import { CommandAuthorizationService } from '../../commands/command-authorization.service.js'; @@ -51,7 +51,7 @@ function authorization(): CommandAuthorizationService { ); } -describe('Tess Discord/CLI durable-session integration', () => { +describe('interaction Discord/CLI durable-session integration', () => { afterEach(() => { for (const key of envKeys) { const value = priorEnv.get(key); @@ -80,7 +80,7 @@ describe('Tess Discord/CLI durable-session integration', () => { }, ]); - const durable = new TessDurableSessionService( + const durable = new DurableSessionService( new InMemoryDurableSessionStore() as never, {} as never, ); diff --git a/apps/gateway/src/agent/agent.module.ts b/apps/gateway/src/agent/agent.module.ts index 0c8b1b6..0ec2cef 100644 --- a/apps/gateway/src/agent/agent.module.ts +++ b/apps/gateway/src/agent/agent.module.ts @@ -11,8 +11,8 @@ import { SessionsController } from './sessions.controller.js'; import { AgentConfigsController } from './agent-configs.controller.js'; import { InteractionController } from './interaction.controller.js'; import { RoutingController } from './routing/routing.controller.js'; -import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; -import { TessDurableSessionService } from './tess-durable-session.service.js'; +import { DurableSessionRepository } from './durable-session.repository.js'; +import { DurableSessionService } from './durable-session.service.js'; import { CoordModule } from '../coord/coord.module.js'; import { McpClientModule } from '../mcp-client/mcp-client.module.js'; import { SkillsModule } from '../skills/skills.module.js'; @@ -44,8 +44,8 @@ export function createGatewayRuntimeProviderRegistry(): AgentRuntimeProviderRegi RoutingService, RoutingEngineService, SkillLoaderService, - TessDurableSessionRepository, - TessDurableSessionService, + DurableSessionRepository, + DurableSessionService, { provide: AGENT_RUNTIME_PROVIDER_REGISTRY, useFactory: createGatewayRuntimeProviderRegistry, @@ -76,7 +76,7 @@ export function createGatewayRuntimeProviderRegistry(): AgentRuntimeProviderRegi RoutingService, RoutingEngineService, SkillLoaderService, - TessDurableSessionService, + DurableSessionService, RuntimeProviderService, AGENT_RUNTIME_PROVIDER_REGISTRY, ], diff --git a/apps/gateway/src/agent/tess-durable-session.dto.ts b/apps/gateway/src/agent/durable-session.dto.ts similarity index 87% rename from apps/gateway/src/agent/tess-durable-session.dto.ts rename to apps/gateway/src/agent/durable-session.dto.ts index 9248797..5280505 100644 --- a/apps/gateway/src/agent/tess-durable-session.dto.ts +++ b/apps/gateway/src/agent/durable-session.dto.ts @@ -1,7 +1,7 @@ import type { RuntimeProviderRequestContext } from './runtime-provider-registry.service.js'; /** Server-side request for a replay-safe provider message. */ -export interface TessProviderOutboxDto { +export interface ProviderOutboxDto { sessionId: string; idempotencyKey: string; correlationId: string; diff --git a/apps/gateway/src/agent/tess-durable-session.repository.test.ts b/apps/gateway/src/agent/durable-session.repository.test.ts similarity index 91% rename from apps/gateway/src/agent/tess-durable-session.repository.test.ts rename to apps/gateway/src/agent/durable-session.repository.test.ts index c194499..b69dcae 100644 --- a/apps/gateway/src/agent/tess-durable-session.repository.test.ts +++ b/apps/gateway/src/agent/durable-session.repository.test.ts @@ -6,8 +6,8 @@ import { eq, sql, interactionCheckpoints, interactionInbox } from '@mosaicstack/ import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent'; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { createPgliteDb, runPgliteMigrations, type DbHandle } from '@mosaicstack/db'; -import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; -import { TessDurableSessionService } from './tess-durable-session.service.js'; +import { DurableSessionRepository } from './durable-session.repository.js'; +import { DurableSessionService } from './durable-session.service.js'; const IDENTITY: DurableSessionIdentity = { agentName: 'Nova', @@ -18,7 +18,7 @@ const IDENTITY: DurableSessionIdentity = { runtimeSessionId: 'nova', }; -describe('TessDurableSessionRepository', () => { +describe('DurableSessionRepository', () => { let dataDir: string | undefined; let handle: DbHandle; let previousAuthSecret: string | undefined; @@ -48,9 +48,7 @@ describe('TessDurableSessionRepository', () => { }); it('survives a full PGlite close/reopen mid-session without duplicate inbox or outbox side effects', async () => { - const beforeRestart = new DurableSessionCoordinator( - new TessDurableSessionRepository(handle.db), - ); + const beforeRestart = new DurableSessionCoordinator(new DurableSessionRepository(handle.db)); await beforeRestart.create(IDENTITY); await beforeRestart.receive({ sessionId: IDENTITY.sessionId, @@ -92,7 +90,7 @@ describe('TessDurableSessionRepository', () => { await handle.close(); handle = createPgliteDb(dataDir!); - const afterRestart = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + const afterRestart = new DurableSessionCoordinator(new DurableSessionRepository(handle.db)); const recovered = await afterRestart.recover(IDENTITY.sessionId); const resumedHandoff = await afterRestart.resumeHandoff('handoff-before-kill'); const handled: string[] = []; @@ -120,7 +118,7 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('redacts sensitive durable payloads before persistence', async () => { - const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db)); await coordinator.create(IDENTITY); await coordinator.receive({ sessionId: IDENTITY.sessionId, @@ -157,7 +155,7 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('fails closed when the configured idempotency secret is unavailable', async () => { - const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db)); await coordinator.create(IDENTITY); const secret = process.env['BETTER_AUTH_SECRET']; delete process.env['BETTER_AUTH_SECRET']; @@ -177,7 +175,7 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('uses keyed pre-redaction digests to reject distinct sensitive checkpoint payloads', async () => { - const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db)); await coordinator.create(IDENTITY); const input = { sessionId: IDENTITY.sessionId, @@ -227,7 +225,7 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('rejects distinct sensitive inbox and outbox payloads under reused idempotency keys', async () => { - const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db)); await coordinator.create(IDENTITY); const inbox = { sessionId: IDENTITY.sessionId, @@ -255,7 +253,7 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('rejects database inbox and outbox idempotency-key conflicts', async () => { - const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db)); await coordinator.create(IDENTITY); await coordinator.receive({ sessionId: IDENTITY.sessionId, @@ -293,10 +291,10 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('does not requeue a live outbox claim during a normal scoped dispatch', async () => { - const repository = new TessDurableSessionRepository(handle.db); + const repository = new DurableSessionRepository(handle.db); const coordinator = new DurableSessionCoordinator(repository); const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; - const service = new TessDurableSessionService(repository, runtimeProviders as never); + const service = new DurableSessionService(repository, runtimeProviders as never); const input = { sessionId: IDENTITY.sessionId, idempotencyKey: 'live-effect', @@ -333,10 +331,10 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('rejects an outbox correlation mismatch before claiming the pending effect', async () => { - const repository = new TessDurableSessionRepository(handle.db); + const repository = new DurableSessionRepository(handle.db); const coordinator = new DurableSessionCoordinator(repository); const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; - const service = new TessDurableSessionService(repository, runtimeProviders as never); + const service = new DurableSessionService(repository, runtimeProviders as never); const input = { sessionId: IDENTITY.sessionId, idempotencyKey: 'mismatch-effect', @@ -366,10 +364,10 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('dispatches only the outbox record bound to the supplied correlation and channel', async () => { - const repository = new TessDurableSessionRepository(handle.db); + const repository = new DurableSessionRepository(handle.db); const coordinator = new DurableSessionCoordinator(repository); const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; - const service = new TessDurableSessionService(repository, runtimeProviders as never); + const service = new DurableSessionService(repository, runtimeProviders as never); const first = { sessionId: IDENTITY.sessionId, idempotencyKey: 'scoped-effect-one', diff --git a/apps/gateway/src/agent/tess-durable-session.repository.ts b/apps/gateway/src/agent/durable-session.repository.ts similarity index 95% rename from apps/gateway/src/agent/tess-durable-session.repository.ts rename to apps/gateway/src/agent/durable-session.repository.ts index 52d853b..bc000a8 100644 --- a/apps/gateway/src/agent/tess-durable-session.repository.ts +++ b/apps/gateway/src/agent/durable-session.repository.ts @@ -33,7 +33,7 @@ import type { import { DB } from '../database/database.module.js'; @Injectable() -export class TessDurableSessionRepository implements DurableSessionStore { +export class DurableSessionRepository implements DurableSessionStore { constructor(@Inject(DB) private readonly db: Db) {} async create(identity: DurableSessionIdentity): Promise { @@ -51,7 +51,7 @@ export class TessDurableSessionRepository implements DurableSessionStore { const existing = await this.session(identity.sessionId); if (!existing || !sameEnrollmentScope(existing, identity)) { - throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`); + throw new Error(`Durable session identity conflict: ${identity.sessionId}`); } // A recovered/re-enrolled runtime can receive a new provider session ID; // the conversation handle and owner scope remain immutable. @@ -135,13 +135,13 @@ export class TessDurableSessionRepository implements DurableSessionStore { ), ) .limit(1); - if (!existing[0]) throw new Error(`Durable Tess inbox enqueue failed: ${input.idempotencyKey}`); + if (!existing[0]) throw new Error(`Durable inbox enqueue failed: ${input.idempotencyKey}`); const entry = toInbox(existing[0]); if ( !sameInbox(entry, record) || !matchesContentDigest(existing[0].contentDigest, input.content) ) { - throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); + throw new Error(`Durable inbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: entry.status }; } @@ -224,14 +224,13 @@ export class TessDurableSessionRepository implements DurableSessionStore { ), ) .limit(1); - if (!existing[0]) - throw new Error(`Durable Tess outbox enqueue failed: ${input.idempotencyKey}`); + if (!existing[0]) throw new Error(`Durable outbox enqueue failed: ${input.idempotencyKey}`); const entry = toOutbox(existing[0]); if ( !sameOutbox(entry, record) || !matchesContentDigest(existing[0].contentDigest, input.content) ) { - throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); + throw new Error(`Durable outbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: entry.status }; } @@ -338,7 +337,7 @@ export class TessDurableSessionRepository implements DurableSessionStore { !sameCheckpoint(toCheckpoint(existing[0]), checkpoint) || !matchesCheckpointDigest(existing[0].contentDigest, digest) ) { - throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`); + throw new Error(`Durable checkpoint identity conflict: ${input.checkpointId}`); } } @@ -360,7 +359,7 @@ export class TessDurableSessionRepository implements DurableSessionStore { async handoff(input: DurableHandoffInput): Promise { const checkpoint = await this.findCheckpoint(input.sessionId, input.checkpointId); if (!checkpoint) { - throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); + throw new Error(`Durable handoff checkpoint is unavailable: ${input.checkpointId}`); } const inserted = await this.db .insert(interactionHandoffs) @@ -371,7 +370,7 @@ export class TessDurableSessionRepository implements DurableSessionStore { const existing = await this.findHandoff(input.handoffId); if (!existing || !sameHandoff(existing, input)) { - throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`); + throw new Error(`Durable handoff identity conflict: ${input.handoffId}`); } } diff --git a/apps/gateway/src/agent/tess-durable-session.service.ts b/apps/gateway/src/agent/durable-session.service.ts similarity index 78% rename from apps/gateway/src/agent/tess-durable-session.service.ts rename to apps/gateway/src/agent/durable-session.service.ts index 340694f..a2384fd 100644 --- a/apps/gateway/src/agent/tess-durable-session.service.ts +++ b/apps/gateway/src/agent/durable-session.service.ts @@ -1,23 +1,23 @@ import { ForbiddenException, Inject, Injectable } from '@nestjs/common'; import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent'; -import type { TessProviderOutboxDto } from './tess-durable-session.dto.js'; -import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; +import type { ProviderOutboxDto } from './durable-session.dto.js'; +import { DurableSessionRepository } from './durable-session.repository.js'; import { RuntimeProviderService, type RuntimeProviderRequestContext, } from './runtime-provider-registry.service.js'; /** - * Scoped gateway boundary for the canonical Tess state machine. It deliberately + * Scoped gateway boundary for the canonical durable session state machine. It deliberately * uses composition: raw state methods cannot be injected into channel, CLI, or * MCP adapters without a server-derived actor/tenant/correlation context. */ @Injectable() -export class TessDurableSessionService { +export class DurableSessionService { private readonly coordinator: DurableSessionCoordinator; constructor( - @Inject(TessDurableSessionRepository) repository: TessDurableSessionRepository, + @Inject(DurableSessionRepository) repository: DurableSessionRepository, @Inject(RuntimeProviderService) private readonly runtimeProviders: RuntimeProviderService, ) { this.coordinator = new DurableSessionCoordinator(repository); @@ -32,12 +32,12 @@ export class TessDurableSessionService { identity.ownerId !== context.actorScope.userId || identity.tenantId !== context.actorScope.tenantId ) { - throw new ForbiddenException('Durable Tess enrollment scope mismatch'); + throw new ForbiddenException('Durable session enrollment scope mismatch'); } await this.coordinator.create(identity); } - async queueProviderSend(input: TessProviderOutboxDto): Promise { + async queueProviderSend(input: ProviderOutboxDto): Promise { const snapshot = await this.coordinator.snapshot(input.sessionId); this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); await this.coordinator.enqueueOutbox({ @@ -50,9 +50,9 @@ export class TessDurableSessionService { }); } - async dispatchProviderOutbox(sessionId: string, input: TessProviderOutboxDto): Promise { + async dispatchProviderOutbox(sessionId: string, input: ProviderOutboxDto): Promise { if (sessionId !== input.sessionId) { - throw new ForbiddenException('Durable Tess outbox session mismatch'); + throw new ForbiddenException('Durable outbox session mismatch'); } const snapshot = await this.coordinator.snapshot(sessionId); this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); @@ -92,7 +92,7 @@ export class TessDurableSessionService { } /** Startup/recovery-only path; normal queue/dispatch methods never requeue live work. */ - async recoverProviderSession(sessionId: string, input: TessProviderOutboxDto): Promise { + async recoverProviderSession(sessionId: string, input: ProviderOutboxDto): Promise { const snapshot = await this.coordinator.snapshot(sessionId); this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); await this.coordinator.recover(sessionId); @@ -100,24 +100,24 @@ export class TessDurableSessionService { private assertOutboxScope( entry: { kind: string; correlationId: string; channelId: string }, - input: TessProviderOutboxDto, + input: ProviderOutboxDto, ): void { if ( entry.kind !== 'provider.send' || entry.correlationId !== input.correlationId || entry.channelId !== input.context.channelId ) { - throw new ForbiddenException('Durable Tess outbox scope or correlation mismatch'); + throw new ForbiddenException('Durable outbox scope or correlation mismatch'); } } - private assertScope(ownerId: string, tenantId: string, input: TessProviderOutboxDto): void { + private assertScope(ownerId: string, tenantId: string, input: ProviderOutboxDto): void { if ( input.context.actorScope.userId !== ownerId || input.context.actorScope.tenantId !== tenantId || input.context.correlationId !== input.correlationId ) { - throw new ForbiddenException('Durable Tess session scope or correlation mismatch'); + throw new ForbiddenException('Durable session scope or correlation mismatch'); } } } diff --git a/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts b/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts index 625bd97..546e64e 100644 --- a/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts +++ b/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts @@ -21,8 +21,8 @@ import { RUNTIME_PROVIDER_AUDIT_SINK, RuntimeProviderAuditService, } from './runtime-provider-registry.service.js'; -import { TessDurableSessionService } from './tess-durable-session.service.js'; -import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; +import { DurableSessionService } from './durable-session.service.js'; +import { DurableSessionRepository } from './durable-session.repository.js'; import { AgentService } from './agent.service.js'; import { ProviderService } from './provider.service.js'; import { ProviderCredentialsService } from './provider-credentials.service.js'; @@ -88,9 +88,9 @@ describe('Hermes runtime provider reachability', (): void => { .useValue({ record: vi.fn().mockResolvedValue(undefined) }) .overrideProvider(RUNTIME_APPROVAL_VERIFIER) .useValue({ consume: vi.fn().mockResolvedValue(false) }) - .overrideProvider(TessDurableSessionService) + .overrideProvider(DurableSessionService) .useValue({}) - .overrideProvider(TessDurableSessionRepository) + .overrideProvider(DurableSessionRepository) .useValue({}) .overrideProvider(AgentService) .useValue({}) diff --git a/apps/gateway/src/agent/interaction.controller.ts b/apps/gateway/src/agent/interaction.controller.ts index 04b77d5..54a4ec7 100644 --- a/apps/gateway/src/agent/interaction.controller.ts +++ b/apps/gateway/src/agent/interaction.controller.ts @@ -17,7 +17,7 @@ import { Observable } from 'rxjs'; import { AuthGuard } from '../auth/auth.guard.js'; import { CurrentUser } from '../auth/current-user.decorator.js'; import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; -import { TessDurableSessionService } from './tess-durable-session.service.js'; +import { DurableSessionService } from './durable-session.service.js'; import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js'; import { RuntimeProviderService, @@ -34,7 +34,7 @@ import { export class InteractionController { constructor( @Inject(RuntimeProviderService) private readonly runtime: RuntimeProviderService, - @Inject(TessDurableSessionService) private readonly durable: TessDurableSessionService, + @Inject(DurableSessionService) private readonly durable: DurableSessionService, ) {} @Get('sessions') diff --git a/apps/gateway/src/chat/chat.gateway.ts b/apps/gateway/src/chat/chat.gateway.ts index a7549e4..9f2541d 100644 --- a/apps/gateway/src/chat/chat.gateway.ts +++ b/apps/gateway/src/chat/chat.gateway.ts @@ -37,7 +37,7 @@ import { RuntimeProviderService, type RuntimeAuditSink, } from '../agent/runtime-provider-registry.service.js'; -import { TessDurableSessionService } from '../agent/tess-durable-session.service.js'; +import { DurableSessionService } from '../agent/durable-session.service.js'; import { AUTH } from '../auth/auth.tokens.js'; import { scopeFromUser, @@ -140,8 +140,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa @Inject(RuntimeProviderService) private readonly runtimeRegistry: RuntimeProviderService | null = null, @Optional() - @Inject(TessDurableSessionService) - private readonly durableSessions: TessDurableSessionService | null = null, + @Inject(DurableSessionService) + private readonly durableSessions: DurableSessionService | null = null, @Optional() @Inject(RUNTIME_PROVIDER_AUDIT_SINK) private readonly runtimeAudit: RuntimeAuditSink | null = null, diff --git a/apps/gateway/src/coord/coord.module.ts b/apps/gateway/src/coord/coord.module.ts index 170280d..8c279a2 100644 --- a/apps/gateway/src/coord/coord.module.ts +++ b/apps/gateway/src/coord/coord.module.ts @@ -1,31 +1,32 @@ import { Module } from '@nestjs/common'; -import { InMemoryMosCoordinationPort } from '@mosaicstack/coord'; +import { InMemoryInteractionCoordinationPort } from '@mosaicstack/coord'; import { CoordService } from './coord.service.js'; import { CoordController } from './coord.controller.js'; -import { MosCoordinationController } from './mos-coordination.controller.js'; +import { InteractionCoordinationController } from './interaction-coordination.controller.js'; import { - MOS_COORDINATION_CONFIG, - MOS_COORDINATION_PORT, - MosCoordinationService, -} from './mos-coordination.service.js'; + COORDINATION_CONFIG, + COORDINATION_PORT, + InteractionCoordinationService, +} from './interaction-coordination.service.js'; @Module({ providers: [ CoordService, { - provide: MOS_COORDINATION_PORT, - useFactory: (): InMemoryMosCoordinationPort => new InMemoryMosCoordinationPort(), + provide: COORDINATION_PORT, + useFactory: (): InMemoryInteractionCoordinationPort => + new InMemoryInteractionCoordinationPort(), }, { - provide: MOS_COORDINATION_CONFIG, + provide: COORDINATION_CONFIG, useFactory: () => ({ interactionAgentId: process.env['MOSAIC_AGENT_NAME'], orchestrationAgentId: process.env['MOSAIC_ORCHESTRATOR_AGENT_NAME'], }), }, - MosCoordinationService, + InteractionCoordinationService, ], - controllers: [CoordController, MosCoordinationController], - exports: [CoordService, MosCoordinationService], + controllers: [CoordController, InteractionCoordinationController], + exports: [CoordService, InteractionCoordinationService], }) export class CoordModule {} diff --git a/apps/gateway/src/coord/mos-coordination.controller.test.ts b/apps/gateway/src/coord/interaction-coordination.controller.test.ts similarity index 78% rename from apps/gateway/src/coord/mos-coordination.controller.test.ts rename to apps/gateway/src/coord/interaction-coordination.controller.test.ts index 0bec4b7..6974fcc 100644 --- a/apps/gateway/src/coord/mos-coordination.controller.test.ts +++ b/apps/gateway/src/coord/interaction-coordination.controller.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it, vi } from 'vitest'; -import { MosCoordinationController } from './mos-coordination.controller.js'; +import { InteractionCoordinationController } from './interaction-coordination.controller.js'; const user = { id: 'operator-1', tenantId: 'tenant-a' }; -describe('MosCoordinationController', () => { +describe('InteractionCoordinationController', () => { it('derives actor and tenant from the authenticated user rather than handoff input', async () => { const coordination = { handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })), observe: vi.fn(), result: vi.fn(), }; - const controller = new MosCoordinationController(coordination as never); + const controller = new InteractionCoordinationController(coordination as never); await controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, 'corr-1'); @@ -26,7 +26,7 @@ describe('MosCoordinationController', () => { it('requires a correlation header before invoking the coordination service', async () => { const coordination = { handoff: vi.fn(), observe: vi.fn(), result: vi.fn() }; - const controller = new MosCoordinationController(coordination as never); + const controller = new InteractionCoordinationController(coordination as never); await expect( controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, undefined), diff --git a/apps/gateway/src/coord/mos-coordination.controller.ts b/apps/gateway/src/coord/interaction-coordination.controller.ts similarity index 73% rename from apps/gateway/src/coord/mos-coordination.controller.ts rename to apps/gateway/src/coord/interaction-coordination.controller.ts index cacfe7b..2588a76 100644 --- a/apps/gateway/src/coord/mos-coordination.controller.ts +++ b/apps/gateway/src/coord/interaction-coordination.controller.ts @@ -14,27 +14,28 @@ import { CurrentUser } from '../auth/current-user.decorator.js'; import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; import type { - MosCoordinationObservationDto, - MosCoordinationResponseDto, - MosCoordinationResultDto, - CreateMosHandoffDto, -} from './mos-coordination.dto.js'; -import { MosCoordinationService } from './mos-coordination.service.js'; + InteractionCoordinationObservationDto, + InteractionCoordinationResponseDto, + InteractionCoordinationResultDto, + CreateHandoffDto, +} from './interaction-coordination.dto.js'; +import { InteractionCoordinationService } from './interaction-coordination.service.js'; -/** Authenticated interaction-plane boundary for the handoff/observe/result-only Mos contract. */ +/** Authenticated interaction-plane boundary for the handoff/observe/result-only interaction coordination contract. */ @Controller('api/coord/mos') @UseGuards(AuthGuard) -export class MosCoordinationController { +export class InteractionCoordinationController { constructor( - @Inject(MosCoordinationService) private readonly coordination: MosCoordinationService, + @Inject(InteractionCoordinationService) + private readonly coordination: InteractionCoordinationService, ) {} @Post('handoff') async handoff( - @Body() request: CreateMosHandoffDto, + @Body() request: CreateHandoffDto, @CurrentUser() user: AuthenticatedUserLike, @Headers('x-correlation-id') correlationId?: string, - ): Promise { + ): Promise { return { receipt: await this.coordination.handoff(request, this.context(user, correlationId)) }; } @@ -43,7 +44,7 @@ export class MosCoordinationController { @Param('handoffId') handoffId: string, @CurrentUser() user: AuthenticatedUserLike, @Headers('x-correlation-id') correlationId?: string, - ): Promise { + ): Promise { return { observation: await this.coordination.observe(handoffId, this.context(user, correlationId)), }; @@ -54,7 +55,7 @@ export class MosCoordinationController { @Param('handoffId') handoffId: string, @CurrentUser() user: AuthenticatedUserLike, @Headers('x-correlation-id') correlationId?: string, - ): Promise { + ): Promise { return { result: await this.coordination.result(handoffId, this.context(user, correlationId)) }; } diff --git a/apps/gateway/src/coord/mos-coordination.dto.ts b/apps/gateway/src/coord/interaction-coordination.dto.ts similarity index 59% rename from apps/gateway/src/coord/mos-coordination.dto.ts rename to apps/gateway/src/coord/interaction-coordination.dto.ts index 7967753..be8d5e9 100644 --- a/apps/gateway/src/coord/mos-coordination.dto.ts +++ b/apps/gateway/src/coord/interaction-coordination.dto.ts @@ -1,25 +1,25 @@ import type { CoordinationObservation, CoordinationResult, - MosHandoffReceipt, + HandoffReceipt, } from '@mosaicstack/coord'; /** Input accepted at the gateway coordination boundary. Agent identity is not caller-controlled. */ -export interface CreateMosHandoffDto { +export interface CreateHandoffDto { idempotencyKey: string; summary: string; context?: string; missionId?: string; } -export interface MosCoordinationResponseDto { - receipt: MosHandoffReceipt; +export interface InteractionCoordinationResponseDto { + receipt: HandoffReceipt; } -export interface MosCoordinationObservationDto { +export interface InteractionCoordinationObservationDto { observation: CoordinationObservation; } -export interface MosCoordinationResultDto { +export interface InteractionCoordinationResultDto { result: CoordinationResult; } diff --git a/apps/gateway/src/coord/mos-coordination.service.test.ts b/apps/gateway/src/coord/interaction-coordination.service.test.ts similarity index 79% rename from apps/gateway/src/coord/mos-coordination.service.test.ts rename to apps/gateway/src/coord/interaction-coordination.service.test.ts index 51b2a26..f9eadbb 100644 --- a/apps/gateway/src/coord/mos-coordination.service.test.ts +++ b/apps/gateway/src/coord/interaction-coordination.service.test.ts @@ -1,15 +1,15 @@ import { describe, expect, it, vi } from 'vitest'; import { - InMemoryMosCoordinationPort, - type MosCoordinationPort, - type MosHandoff, + InMemoryInteractionCoordinationPort, + type InteractionCoordinationPort, + type Handoff, } from '@mosaicstack/coord'; import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; import { - MosCoordinationService, - type MosCoordinationConfig, - type MosCoordinationGatewayError, -} from './mos-coordination.service.js'; + InteractionCoordinationService, + type InteractionCoordinationConfig, + type InteractionCoordinationGatewayError, +} from './interaction-coordination.service.js'; const context: RuntimeProviderRequestContext = { actorScope: { userId: 'operator-1', tenantId: 'tenant-a' }, @@ -17,28 +17,28 @@ const context: RuntimeProviderRequestContext = { correlationId: 'corr-1', }; -const config: MosCoordinationConfig = { +const config: InteractionCoordinationConfig = { interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor', }; function service( - port: MosCoordinationPort = new InMemoryMosCoordinationPort(), + port: InteractionCoordinationPort = new InMemoryInteractionCoordinationPort(), options: { - config?: MosCoordinationConfig; + config?: InteractionCoordinationConfig; handoffIdFactory?: () => string; } = {}, -): MosCoordinationService { - return new MosCoordinationService( +): InteractionCoordinationService { + return new InteractionCoordinationService( port, options.config ?? config, options.handoffIdFactory ?? (() => 'handoff-1'), ); } -describe('MosCoordinationService authority boundary', (): void => { +describe('InteractionCoordinationService authority boundary', (): void => { it('derives identity and actor/tenant scope server-side, then round-trips the native adapter', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + const adapter = new InMemoryInteractionCoordinationPort(); const coordination = service(adapter); await expect( @@ -53,8 +53,8 @@ describe('MosCoordinationService authority boundary', (): void => { correlationId: 'corr-1', }); - adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request'); - adapter.recordResult('handoff-1', 'completed', 'Merged by Mos'); + adapter.recordActivity('handoff-1', 'running', 'Orchestrator accepted the request'); + adapter.recordResult('handoff-1', 'completed', 'Merged by orchestrator'); const followUpContext = { ...context, correlationId: 'corr-2' }; await expect(coordination.observe('handoff-1', followUpContext)).resolves.toMatchObject({ @@ -64,12 +64,12 @@ describe('MosCoordinationService authority boundary', (): void => { await expect(coordination.result('handoff-1', followUpContext)).resolves.toMatchObject({ targetAgentId: 'Conductor', status: 'completed', - summary: 'Merged by Mos', + summary: 'Merged by orchestrator', }); }); it('fails closed without calling a port when the interaction requester is unconfigured', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + const adapter = new InMemoryInteractionCoordinationPort(); const handoff = vi.spyOn(adapter, 'handoff'); const coordination = service(adapter, { config: { interactionAgentId: '', orchestrationAgentId: 'Conductor' }, @@ -82,12 +82,12 @@ describe('MosCoordinationService authority boundary', (): void => { ), ).rejects.toMatchObject({ code: 'unconfigured_requester', - } satisfies Partial); + } satisfies Partial); expect(handoff).not.toHaveBeenCalled(); }); it('rejects self-delegation configuration before delivering work', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + const adapter = new InMemoryInteractionCoordinationPort(); const handoff = vi.spyOn(adapter, 'handoff'); const coordination = service(adapter, { config: { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' }, @@ -103,7 +103,7 @@ describe('MosCoordinationService authority boundary', (): void => { }); it('denies cross-tenant observe and result before calling the adapter', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + const adapter = new InMemoryInteractionCoordinationPort(); const observe = vi.spyOn(adapter, 'observe'); const result = vi.spyOn(adapter, 'result'); const coordination = service(adapter); @@ -118,10 +118,10 @@ describe('MosCoordinationService authority boundary', (): void => { }; await expect(coordination.observe('handoff-1', otherTenant)).rejects.toMatchObject({ code: 'cross_tenant_forbidden', - } satisfies Partial); + } satisfies Partial); await expect(coordination.result('handoff-1', otherTenant)).rejects.toMatchObject({ code: 'cross_tenant_forbidden', - } satisfies Partial); + } satisfies Partial); expect(observe).not.toHaveBeenCalled(); expect(result).not.toHaveBeenCalled(); }); @@ -132,8 +132,8 @@ describe('MosCoordinationService authority boundary', (): void => { const delivered = new Promise((resolve: () => void): void => { release = resolve; }); - const adapter: MosCoordinationPort = { - handoff: vi.fn(async (handoff: MosHandoff) => { + const adapter: InteractionCoordinationPort = { + handoff: vi.fn(async (handoff: Handoff) => { await delivered; return { handoffId: handoff.handoffId, @@ -169,7 +169,7 @@ describe('MosCoordinationService authority boundary', (): void => { }); it('rejects idempotency-key payload drift and malformed handoff input before delivery', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + const adapter = new InMemoryInteractionCoordinationPort(); const handoff = vi.spyOn(adapter, 'handoff'); const coordination = service(adapter); await coordination.handoff( @@ -181,23 +181,23 @@ describe('MosCoordinationService authority boundary', (): void => { coordination.handoff({ idempotencyKey: 'request-1', summary: 'Different work' }, context), ).rejects.toMatchObject({ code: 'handoff_conflict', - } satisfies Partial); + } satisfies Partial); await expect( coordination.handoff({ idempotencyKey: 'request-2', summary: '' }, context), ).rejects.toMatchObject({ code: 'invalid_request', - } satisfies Partial); + } satisfies Partial); await expect( coordination.handoff({ idempotencyKey: 'request-3', summary: 'x'.repeat(2_049) }, context), ).rejects.toMatchObject({ code: 'invalid_request', - } satisfies Partial); + } satisfies Partial); expect(handoff).toHaveBeenCalledTimes(1); }); it('fails closed when the port reports a target that drifts from configuration', async (): Promise => { - const adapter: MosCoordinationPort = { - handoff: vi.fn(async (handoff: MosHandoff) => ({ + const adapter: InteractionCoordinationPort = { + handoff: vi.fn(async (handoff: Handoff) => ({ handoffId: handoff.handoffId, targetAgentId: 'Unexpected', status: 'accepted' as const, diff --git a/apps/gateway/src/coord/mos-coordination.service.ts b/apps/gateway/src/coord/interaction-coordination.service.ts similarity index 75% rename from apps/gateway/src/coord/mos-coordination.service.ts rename to apps/gateway/src/coord/interaction-coordination.service.ts index bc4bb98..cf16e64 100644 --- a/apps/gateway/src/coord/mos-coordination.service.ts +++ b/apps/gateway/src/coord/interaction-coordination.service.ts @@ -1,18 +1,18 @@ import { Inject, Injectable } from '@nestjs/common'; import { - MosCoordinationClient, + InteractionCoordinationClient, type CoordinationObservation, type CoordinationResult, type CoordinationScope, - type MosCoordinationIdentity, - type MosCoordinationPort, - type MosHandoffReceipt, + type InteractionCoordinationIdentity, + type InteractionCoordinationPort, + type HandoffReceipt, } from '@mosaicstack/coord'; import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; -import type { CreateMosHandoffDto } from './mos-coordination.dto.js'; +import type { CreateHandoffDto } from './interaction-coordination.dto.js'; -export const MOS_COORDINATION_PORT = Symbol('MOS_COORDINATION_PORT'); -export const MOS_COORDINATION_CONFIG = Symbol('MOS_COORDINATION_CONFIG'); +export const COORDINATION_PORT = Symbol('COORDINATION_PORT'); +export const COORDINATION_CONFIG = Symbol('COORDINATION_CONFIG'); const HANDOFF_TRACKING_TTL_MS = 60 * 60 * 1_000; const MAX_TRACKED_HANDOFFS = 1_000; @@ -21,7 +21,7 @@ const MAX_SUMMARY_LENGTH = 2_048; const MAX_CONTEXT_LENGTH = 8_192; const MAX_MISSION_ID_LENGTH = 128; -export interface MosCoordinationConfig { +export interface InteractionCoordinationConfig { interactionAgentId?: string; orchestrationAgentId?: string; } @@ -34,7 +34,7 @@ interface HandoffOwner { expiresAt: number; } -interface NormalizedMosHandoffRequest { +interface NormalizedHandoffRequest { idempotencyKey: string; summary: string; context?: string; @@ -42,31 +42,31 @@ interface NormalizedMosHandoffRequest { } interface TrackedHandoff { - request: NormalizedMosHandoffRequest; - receipt: Promise; + request: NormalizedHandoffRequest; + receipt: Promise; expiresAt: number; } /** * Gateway authority boundary for the interaction agent. It derives requester, * actor, and tenant from trusted server configuration and authentication; no - * channel request can name a target or gain Mos-owned orchestration verbs. + * channel request can name a target or gain orchestrator-owned orchestration verbs. */ @Injectable() -export class MosCoordinationService { +export class InteractionCoordinationService { private readonly owners = new Map(); private readonly handoffsByIdempotencyKey = new Map(); constructor( - @Inject(MOS_COORDINATION_PORT) private readonly port: MosCoordinationPort, - @Inject(MOS_COORDINATION_CONFIG) private readonly config: MosCoordinationConfig, + @Inject(COORDINATION_PORT) private readonly port: InteractionCoordinationPort, + @Inject(COORDINATION_CONFIG) private readonly config: InteractionCoordinationConfig, private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(), ) {} async handoff( - request: CreateMosHandoffDto, + request: CreateHandoffDto, context: RuntimeProviderRequestContext, - ): Promise { + ): Promise { this.pruneExpiredTracking(); const normalized = this.normalizeRequest(request); const scope = this.scope(context); @@ -74,9 +74,9 @@ export class MosCoordinationService { const existing = this.handoffsByIdempotencyKey.get(idempotencyKey); if (existing !== undefined) { if (!sameRequest(existing.request, normalized)) { - throw new MosCoordinationGatewayError( + throw new InteractionCoordinationGatewayError( 'handoff_conflict', - 'Mos handoff idempotency key is already bound to different immutable input', + 'Handoff idempotency key is already bound to different immutable input', ); } return existing.receipt; @@ -122,9 +122,9 @@ export class MosCoordinationService { private async deliverHandoff( handoffId: string, - request: NormalizedMosHandoffRequest, + request: NormalizedHandoffRequest, scope: CoordinationScope, - ): Promise { + ): Promise { const receipt = await this.client((): string => handoffId).handoff(request, scope); const owner: HandoffOwner = { actorId: scope.actorId, @@ -135,9 +135,9 @@ export class MosCoordinationService { }; const existing = this.owners.get(receipt.handoffId); if (existing !== undefined && !sameOwner(existing, owner)) { - throw new MosCoordinationGatewayError( + throw new InteractionCoordinationGatewayError( 'handoff_conflict', - 'Mos handoff ID is already bound to a different authenticated scope', + 'Handoff ID is already bound to a different authenticated scope', ); } this.owners.set(receipt.handoffId, owner); @@ -145,21 +145,21 @@ export class MosCoordinationService { return receipt; } - private client(handoffIdFactory?: () => string): MosCoordinationClient { - return new MosCoordinationClient(this.identity(), this.port, handoffIdFactory); + private client(handoffIdFactory?: () => string): InteractionCoordinationClient { + return new InteractionCoordinationClient(this.identity(), this.port, handoffIdFactory); } - private identity(): MosCoordinationIdentity { + private identity(): InteractionCoordinationIdentity { const interactionAgentId = this.config.interactionAgentId?.trim(); const orchestrationAgentId = this.config.orchestrationAgentId?.trim(); if (!interactionAgentId) { - throw new MosCoordinationGatewayError( + throw new InteractionCoordinationGatewayError( 'unconfigured_requester', 'Interaction agent identity is not configured', ); } if (!orchestrationAgentId) { - throw new MosCoordinationGatewayError( + throw new InteractionCoordinationGatewayError( 'unconfigured_target', 'Orchestration agent identity is not configured', ); @@ -177,9 +177,12 @@ export class MosCoordinationService { }); } - private normalizeRequest(request: CreateMosHandoffDto): NormalizedMosHandoffRequest { + private normalizeRequest(request: CreateHandoffDto): NormalizedHandoffRequest { if (typeof request !== 'object' || request === null) { - throw new MosCoordinationGatewayError('invalid_request', 'Mos handoff request is invalid'); + throw new InteractionCoordinationGatewayError( + 'invalid_request', + 'Handoff request is invalid', + ); } const idempotencyKey = this.requiredString( request.idempotencyKey, @@ -203,14 +206,17 @@ export class MosCoordinationService { private requiredString(value: unknown, field: string, maximumLength: number): string { if (typeof value !== 'string') { - throw new MosCoordinationGatewayError( + throw new InteractionCoordinationGatewayError( 'invalid_request', - `Mos handoff ${field} must be a string`, + `Handoff ${field} must be a string`, ); } const normalized = value.trim(); if (normalized.length === 0 || normalized.length > maximumLength) { - throw new MosCoordinationGatewayError('invalid_request', `Mos handoff ${field} is invalid`); + throw new InteractionCoordinationGatewayError( + 'invalid_request', + `Handoff ${field} is invalid`, + ); } return normalized; } @@ -245,23 +251,23 @@ export class MosCoordinationService { private ownerFor(handoffId: string, scope: CoordinationScope): HandoffOwner { const owner = this.owners.get(handoffId); if (owner === undefined) { - throw new MosCoordinationGatewayError('not_found', 'Mos handoff was not found'); + throw new InteractionCoordinationGatewayError('not_found', 'Handoff was not found'); } if ( owner.tenantId !== scope.tenantId || owner.actorId !== scope.actorId || owner.requesterAgentId !== scope.requesterAgentId ) { - throw new MosCoordinationGatewayError( + throw new InteractionCoordinationGatewayError( 'cross_tenant_forbidden', - 'Mos handoff is outside the authenticated scope', + 'Handoff is outside the authenticated scope', ); } return owner; } } -export type MosCoordinationGatewayErrorCode = +export type InteractionCoordinationGatewayErrorCode = | 'cross_tenant_forbidden' | 'handoff_conflict' | 'invalid_request' @@ -277,10 +283,7 @@ function sameOwner(left: HandoffOwner, right: HandoffOwner): boolean { ); } -function sameRequest( - left: NormalizedMosHandoffRequest, - right: NormalizedMosHandoffRequest, -): boolean { +function sameRequest(left: NormalizedHandoffRequest, right: NormalizedHandoffRequest): boolean { return ( left.idempotencyKey === right.idempotencyKey && left.summary === right.summary && @@ -289,12 +292,12 @@ function sameRequest( ); } -export class MosCoordinationGatewayError extends Error { +export class InteractionCoordinationGatewayError extends Error { constructor( - readonly code: MosCoordinationGatewayErrorCode, + readonly code: InteractionCoordinationGatewayErrorCode, message: string, ) { super(message); - this.name = MosCoordinationGatewayError.name; + this.name = InteractionCoordinationGatewayError.name; } } diff --git a/docs/scratchpads/747-wsa-dehardcode.md b/docs/scratchpads/747-wsa-dehardcode.md new file mode 100644 index 0000000..655200b --- /dev/null +++ b/docs/scratchpads/747-wsa-dehardcode.md @@ -0,0 +1,43 @@ +# #747 — De-hardcode orchestrator and interaction agent names + +## Objective + +Replace branded Mos/Tess symbols, filenames, DI tokens, and error prose with role-neutral orchestrator/interaction vocabulary without changing env-driven runtime identity behavior. Add optional roster `alias` and `provider` fields and show aliases in `mosaic fleet ps` with name fallback. + +## Scope and constraints + +- Requirements: `/home/hermes/agent-work/reviews/747-wsa-dehardcode-brief.md`. +- Branch: `feat/747-dehardcode-orchestrator-interaction-names` from `main` at `e72388b2`. +- Keep `MOSAIC_AGENT_NAME` and `MOSAIC_ORCHESTRATOR_AGENT_NAME` unchanged. +- Sample/test data may retain operator display names. +- No behavior change beyond optional roster metadata and alias display. +- Budget: no explicit cap; conservative mechanical-rename scope only. +- TDD: optional and skipped because this is a mechanical rename with existing focused coverage; add focused alias/schema regression coverage before completion. + +## Plan + +1. Rename coordination and durable-session files and symbols using canonical vocabulary. +2. Scrub branded symbol names and error prose in the assigned source trees while preserving allowed sample data. +3. Extend roster schema with optional `alias` and `provider`; update fleet roster typing/rendering and focused tests. +4. Run grep-clean verification, build, typecheck, lint/format, focused coord/durable-session/fleet tests, and roster validation. +5. Commit, queue-guard, push, open a Gitea PR closing #747, and report to the coordinator. + +## Progress + +- 2026-07-13: Task resumed from coordinator brief; repository clean at `e72388b2`. +- Renamed coordination and durable-session files, exports, gateway DI symbols, DTOs, services, repositories, and tests. +- Replaced branded authority/error prose while preserving the existing `/api/coord/mos` compatibility route and env-variable identity inputs. +- Added optional roster `alias`/`provider` support and alias-first `fleet ps` display with canonical-name fallback. + +## Verification + +- `pnpm typecheck`: passed (42 tasks). +- `pnpm build`: passed (23 tasks). +- `pnpm lint`: passed (23 tasks). +- `pnpm format:check`: passed. +- Coordination tests: 7 passed. +- Agent durable-session/runtime tests: 23 passed. +- Gateway coordination/durable-session/integration tests: 20 passed. +- Full `fleet.spec.ts`: 192 passed, including alias/provider parsing and alias display. +- JSON Schema 2020 validation: legacy minimal roster and extended alias/provider roster passed; `alias` and `provider` remain absent from `required`. +- Grep verification: no branded symbol/type/file/DI names or error prose remain in assigned source trees; one allowed `Tess Owner` test-data display name remains. diff --git a/packages/agent/src/tess-durable-session.test.ts b/packages/agent/src/durable-session.test.ts similarity index 99% rename from packages/agent/src/tess-durable-session.test.ts rename to packages/agent/src/durable-session.test.ts index 3cadaf4..109702e 100644 --- a/packages/agent/src/tess-durable-session.test.ts +++ b/packages/agent/src/durable-session.test.ts @@ -3,7 +3,7 @@ import { DurableSessionCoordinator, InMemoryDurableSessionStore, type DurableSessionIdentity, -} from './tess-durable-session.js'; +} from './durable-session.js'; const IDENTITY: DurableSessionIdentity = { agentName: 'Nova', diff --git a/packages/agent/src/tess-durable-session.ts b/packages/agent/src/durable-session.ts similarity index 95% rename from packages/agent/src/tess-durable-session.ts rename to packages/agent/src/durable-session.ts index adc84c6..a66e3a5 100644 --- a/packages/agent/src/tess-durable-session.ts +++ b/packages/agent/src/durable-session.ts @@ -102,7 +102,7 @@ export interface DurableSessionStore { export class DurableSessionNotFoundError extends Error { constructor(sessionId: string) { - super(`Durable Tess session not found: ${sessionId}`); + super(`Durable session not found: ${sessionId}`); this.name = 'DurableSessionNotFoundError'; } } @@ -212,11 +212,11 @@ export class DurableSessionCoordinator { async resumeHandoff(handoffId: string): Promise { const handoff = await this.store.findHandoff(handoffId); - if (!handoff) throw new Error(`Durable Tess handoff not found: ${handoffId}`); + if (!handoff) throw new Error(`Durable handoff not found: ${handoffId}`); const snapshot = await this.snapshot(handoff.sessionId); const checkpoint = await this.store.findCheckpoint(handoff.sessionId, handoff.checkpointId); if (!checkpoint) { - throw new Error(`Durable Tess handoff checkpoint is unavailable: ${handoff.checkpointId}`); + throw new Error(`Durable handoff checkpoint is unavailable: ${handoff.checkpointId}`); } return { identity: snapshot.identity, checkpoint, handoff }; } @@ -257,7 +257,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore { const existing = this.sessions.get(identity.sessionId); if (existing) { if (!sameEnrollmentScope(existing.identity, identity)) { - throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`); + throw new Error(`Durable session identity conflict: ${identity.sessionId}`); } existing.identity.providerId = identity.providerId; existing.identity.runtimeSessionId = identity.runtimeSessionId; @@ -290,7 +290,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore { const existing = state.inbox.get(input.idempotencyKey); if (existing) { if (!sameInbox(existing, input)) { - throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); + throw new Error(`Durable inbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: existing.status }; } @@ -323,7 +323,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore { const existing = state.outbox.get(input.idempotencyKey); if (existing) { if (!sameOutbox(existing, input)) { - throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); + throw new Error(`Durable outbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: existing.status }; } @@ -364,7 +364,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore { const checkpoints = this.require(input.sessionId).checkpoints; const existing = checkpoints.get(input.checkpointId); if (existing && !sameCheckpoint(existing, input)) { - throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`); + throw new Error(`Durable checkpoint identity conflict: ${input.checkpointId}`); } if (!existing) checkpoints.set(input.checkpointId, { ...input }); } @@ -377,11 +377,11 @@ export class InMemoryDurableSessionStore implements DurableSessionStore { async handoff(input: DurableHandoffInput): Promise { const state = this.require(input.sessionId); if (!state.checkpoints.has(input.checkpointId)) { - throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); + throw new Error(`Durable handoff checkpoint is unavailable: ${input.checkpointId}`); } const existing = state.handoffs.get(input.handoffId); if (existing && !sameHandoff(existing, input)) { - throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`); + throw new Error(`Durable handoff identity conflict: ${input.handoffId}`); } if (!existing) state.handoffs.set(input.handoffId, { ...input }); } @@ -413,7 +413,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore { kind: string, ): T { const entry = records.get(idempotencyKey); - if (!entry) throw new Error(`Durable Tess ${kind} entry not found: ${idempotencyKey}`); + if (!entry) throw new Error(`Durable ${kind} entry not found: ${idempotencyKey}`); return entry; } } diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 702d9d9..0123793 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -4,4 +4,4 @@ export * from './runtime-provider-registry.js'; export * from './tmux-fleet-runtime-provider.js'; export * from './hermes-runtime-provider.js'; export * from './matrix-native-runtime-provider.js'; -export * from './tess-durable-session.js'; +export * from './durable-session.js'; diff --git a/packages/agent/src/matrix-native-runtime-provider.ts b/packages/agent/src/matrix-native-runtime-provider.ts index 3398514..c32a136 100644 --- a/packages/agent/src/matrix-native-runtime-provider.ts +++ b/packages/agent/src/matrix-native-runtime-provider.ts @@ -117,7 +117,7 @@ class DenyMatrixWriteAuthority implements MatrixWriteAuthority { async assertAuthorized(): Promise { throw new MatrixRuntimeProviderError( 'forbidden', - 'Matrix runtime writes require Mos authority', + 'Matrix runtime writes require orchestrator authority', ); } } diff --git a/packages/agent/src/tmux-fleet-runtime-provider.test.ts b/packages/agent/src/tmux-fleet-runtime-provider.test.ts index 84f0146..2031572 100644 --- a/packages/agent/src/tmux-fleet-runtime-provider.test.ts +++ b/packages/agent/src/tmux-fleet-runtime-provider.test.ts @@ -202,7 +202,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => { expect(fleet.terminate).not.toHaveBeenCalled(); }); - it('rejects an unverified target before consulting Mos write authority', async (): Promise => { + it('rejects an unverified target before consulting orchestrator write authority', async (): Promise => { const fleet = transport(); fleet.verifySession = vi.fn(async (): Promise => { throw new Error('target identity mismatch'); @@ -220,7 +220,20 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => { expect(fleet.sendMessage).not.toHaveBeenCalled(); }); - it('passes an exact session ID to the fleet transport only through authorized Mos writes', async (): Promise => { + it('uses the role-neutral interaction source label by default', async (): Promise => { + const fleet = transport(); + const writeAuthority: FleetWriteAuthority = { + canWrite: vi.fn(async (): Promise => true), + assertAuthorized: vi.fn(async (): Promise => undefined), + }; + const provider = new TmuxFleetRuntimeProvider({ transport: fleet, writeAuthority }); + + await provider.sendMessage('coder0', { content: 'hello', idempotencyKey: 'message-1' }, scope); + + expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'interaction'); + }); + + it('passes an exact session ID to the fleet transport only through orchestrator-authorized writes', async (): Promise => { const fleet = transport(); const writeAuthority: FleetWriteAuthority = { canWrite: vi.fn(async (): Promise => true), @@ -228,7 +241,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => { }; const provider = new TmuxFleetRuntimeProvider({ transport: fleet, - sourceLabel: 'tess', + sourceLabel: 'operator', writeAuthority, }); @@ -246,7 +259,7 @@ describe('TmuxFleetRuntimeProvider security policy', (): void => { scope, approvalRef: 'approval-1', }); - expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'tess'); + expect(fleet.sendMessage).toHaveBeenCalledWith('coder0', 'hello', 'operator'); expect(fleet.terminate).toHaveBeenCalledWith('coder0'); }); diff --git a/packages/agent/src/tmux-fleet-runtime-provider.ts b/packages/agent/src/tmux-fleet-runtime-provider.ts index ed36795..0f0c67b 100644 --- a/packages/agent/src/tmux-fleet-runtime-provider.ts +++ b/packages/agent/src/tmux-fleet-runtime-provider.ts @@ -64,14 +64,14 @@ export interface FleetWriteAuthorization { } /** - * Mos is the only authority that may permit Tess write/control requests to a + * The orchestrator is the only authority that may permit interaction-plane write/control requests to a * fleet peer. Gateway records the request and denial/success around provider - * invocation; the default authority prevents direct Tess writes by design. + * invocation; the default authority prevents direct interaction-plane writes by design. */ export interface FleetWriteAuthority { /** Non-consuming preflight used before probing the fleet transport. */ canWrite(authorization: FleetWriteAuthorization): Promise; - /** Final exact-target authorization; may consume a Mos grant. */ + /** Final exact-target authorization; may consume an orchestrator grant. */ assertAuthorized(authorization: FleetWriteAuthorization): Promise; } @@ -116,7 +116,7 @@ class DenyFleetWriteAuthority implements FleetWriteAuthority { async assertAuthorized(_authorization: FleetWriteAuthorization): Promise { throw new FleetRuntimeProviderError( 'forbidden', - 'Fleet writes require an explicit Mos authority decision', + 'Fleet writes require an explicit orchestrator authority decision', ); } } @@ -124,7 +124,7 @@ class DenyFleetWriteAuthority implements FleetWriteAuthority { /** * A capability-limited provider for rostered local fleet peers. It never * permits raw tmux socket/target selection, interactive control attach, or - * direct Tess writes; all side effects pass through exact transport checks. + * direct interaction-plane writes; all side effects pass through exact transport checks. */ export class TmuxFleetRuntimeProvider implements AgentRuntimeProvider { readonly id = FLEET_PROVIDER_ID; @@ -139,7 +139,7 @@ export class TmuxFleetRuntimeProvider implements AgentRuntimeProvider { constructor(private readonly options: TmuxFleetRuntimeProviderOptions) { this.readAuthority = options.readAuthority ?? new DenyFleetReadAuthority(); this.writeAuthority = options.writeAuthority ?? new DenyFleetWriteAuthority(); - this.sourceLabel = options.sourceLabel ?? 'tess'; + this.sourceLabel = options.sourceLabel ?? 'interaction'; this.attachmentIdFactory = options.attachmentIdFactory ?? randomUUID; this.now = options.now ?? (() => new Date()); this.attachmentTtlMs = options.attachmentTtlMs ?? ATTACHMENT_TTL_MS; diff --git a/packages/coord/src/__tests__/mos-coordination.test.ts b/packages/coord/src/__tests__/interaction-coordination.test.ts similarity index 77% rename from packages/coord/src/__tests__/mos-coordination.test.ts rename to packages/coord/src/__tests__/interaction-coordination.test.ts index 912bdc6..cb64ee3 100644 --- a/packages/coord/src/__tests__/mos-coordination.test.ts +++ b/packages/coord/src/__tests__/interaction-coordination.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from 'vitest'; import { - InMemoryMosCoordinationPort, - MosCoordinationClient, + InMemoryInteractionCoordinationPort, + InteractionCoordinationClient, type CoordinationScope, - type MosCoordinationAuthorityError, - type MosCoordinationPort, + type InteractionCoordinationAuthorityError, + type InteractionCoordinationPort, } from '../index.js'; const scope: CoordinationScope = { @@ -15,19 +15,19 @@ const scope: CoordinationScope = { }; function client( - port: MosCoordinationPort, + port: InteractionCoordinationPort, handoffIdFactory: () => string = (): string => 'handoff-1', -): MosCoordinationClient { - return new MosCoordinationClient( +): InteractionCoordinationClient { + return new InteractionCoordinationClient( { interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor' }, port, handoffIdFactory, ); } -describe('MosCoordinationClient', (): void => { +describe('InteractionCoordinationClient', (): void => { it('round-trips handoff, observation, and result through the native port with identities as data', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + const adapter = new InMemoryInteractionCoordinationPort(); const coordination = client(adapter); await expect( @@ -42,8 +42,8 @@ describe('MosCoordinationClient', (): void => { correlationId: 'corr-1', }); - adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request'); - adapter.recordResult('handoff-1', 'completed', 'Merged by Mos'); + adapter.recordActivity('handoff-1', 'running', 'Orchestrator accepted the request'); + adapter.recordResult('handoff-1', 'completed', 'Merged by orchestrator'); await expect(coordination.observe('handoff-1', scope)).resolves.toMatchObject({ status: 'completed', @@ -59,7 +59,7 @@ describe('MosCoordinationClient', (): void => { targetAgentId: 'Conductor', status: 'completed', correlationId: 'corr-1', - summary: 'Merged by Mos', + summary: 'Merged by orchestrator', }); expect(coordination).not.toHaveProperty('dispatch'); @@ -69,8 +69,8 @@ describe('MosCoordinationClient', (): void => { expect(coordination).not.toHaveProperty('cancel'); }); - it('fails closed before delivery when an unconfigured agent requests Mos work', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + it('fails closed before delivery when an unconfigured agent requests orchestrator work', async (): Promise => { + const adapter = new InMemoryInteractionCoordinationPort(); await expect( client(adapter).handoff( @@ -79,31 +79,31 @@ describe('MosCoordinationClient', (): void => { ), ).rejects.toMatchObject({ code: 'requester_forbidden', - } satisfies Partial); + } satisfies Partial); }); it('rejects self-delegation configuration before constructing a client', (): void => { expect( - (): MosCoordinationClient => - new MosCoordinationClient( + (): InteractionCoordinationClient => + new InteractionCoordinationClient( { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' }, - new InMemoryMosCoordinationPort(), + new InMemoryInteractionCoordinationPort(), ), ).toThrow('Interaction and orchestration identities must differ'); }); it('rejects whitespace-equivalent self-delegation identities', (): void => { expect( - (): MosCoordinationClient => - new MosCoordinationClient( + (): InteractionCoordinationClient => + new InteractionCoordinationClient( { interactionAgentId: 'Nova ', orchestrationAgentId: 'Nova' }, - new InMemoryMosCoordinationPort(), + new InMemoryInteractionCoordinationPort(), ), ).toThrow('Interaction and orchestration identities must differ'); }); it('does not expose another tenant handoff to observe or result', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort(); + const adapter = new InMemoryInteractionCoordinationPort(); const coordination = client(adapter); await coordination.handoff( { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' }, @@ -120,7 +120,7 @@ describe('MosCoordinationClient', (): void => { }); it('bounds native handoff retention by evicting the oldest handoff', async (): Promise => { - const adapter = new InMemoryMosCoordinationPort({ maxHandoffs: 1 }); + const adapter = new InMemoryInteractionCoordinationPort({ maxHandoffs: 1 }); const first = client(adapter, (): string => 'handoff-1'); const second = client(adapter, (): string => 'handoff-2'); await first.handoff({ idempotencyKey: 'handoff-request-1', summary: 'First request' }, scope); @@ -131,7 +131,7 @@ describe('MosCoordinationClient', (): void => { }); it('fails closed when a transport reports target drift', async (): Promise => { - const adapter: MosCoordinationPort = { + const adapter: InteractionCoordinationPort = { handoff: vi.fn(async () => ({ handoffId: 'handoff-1', targetAgentId: 'Unexpected', @@ -149,6 +149,6 @@ describe('MosCoordinationClient', (): void => { ), ).rejects.toMatchObject({ code: 'target_drift', - } satisfies Partial); + } satisfies Partial); }); }); diff --git a/packages/coord/src/in-memory-mos-coordination-port.ts b/packages/coord/src/in-memory-interaction-coordination-port.ts similarity index 77% rename from packages/coord/src/in-memory-mos-coordination-port.ts rename to packages/coord/src/in-memory-interaction-coordination-port.ts index ddebbad..517e2e1 100644 --- a/packages/coord/src/in-memory-mos-coordination-port.ts +++ b/packages/coord/src/in-memory-interaction-coordination-port.ts @@ -2,25 +2,25 @@ import { type CoordinationObservation, type CoordinationResult, type CoordinationScope, - type MosCoordinationActivity, - type MosCoordinationPort, - type MosHandoff, - type MosHandoffReceipt, - type MosHandoffStatus, -} from './mos-coordination.js'; + type InteractionCoordinationActivity, + type InteractionCoordinationPort, + type Handoff, + type HandoffReceipt, + type HandoffStatus, +} from './interaction-coordination.js'; const DEFAULT_HANDOFF_TTL_MS = 60 * 60 * 1_000; const DEFAULT_MAX_HANDOFFS = 1_000; interface StoredHandoff { - readonly handoff: MosHandoff; - status: MosHandoffStatus; - readonly activity: MosCoordinationActivity[]; + readonly handoff: Handoff; + status: HandoffStatus; + readonly activity: InteractionCoordinationActivity[]; readonly expiresAt: number; result?: CoordinationResult; } -export interface InMemoryMosCoordinationPortOptions { +export interface InMemoryInteractionCoordinationPortOptions { now?: () => Date; handoffTtlMs?: number; maxHandoffs?: number; @@ -29,21 +29,21 @@ export interface InMemoryMosCoordinationPortOptions { /** * Native deterministic queue/port adapter for the coordination boundary. * It intentionally has no fleet/tmux dependency. A future deployment adapter - * implements MosCoordinationPort without changing interaction-plane callers. + * implements InteractionCoordinationPort without changing interaction-plane callers. */ -export class InMemoryMosCoordinationPort implements MosCoordinationPort { +export class InMemoryInteractionCoordinationPort implements InteractionCoordinationPort { private readonly handoffs = new Map(); private readonly now: () => Date; private readonly handoffTtlMs: number; private readonly maxHandoffs: number; - constructor(options: InMemoryMosCoordinationPortOptions = {}) { + constructor(options: InMemoryInteractionCoordinationPortOptions = {}) { this.now = options.now ?? (() => new Date()); this.handoffTtlMs = options.handoffTtlMs ?? DEFAULT_HANDOFF_TTL_MS; this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS; } - async handoff(handoff: MosHandoff): Promise { + async handoff(handoff: Handoff): Promise { this.pruneExpiredHandoffs(); const existing = this.handoffs.get(handoff.handoffId); if (existing !== undefined) { @@ -88,14 +88,14 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort { } /** Host-side progression seam; interaction clients never receive this capability. */ - recordActivity(handoffId: string, status: MosHandoffStatus, summary: string): void { + recordActivity(handoffId: string, status: HandoffStatus, summary: string): void { this.pruneExpiredHandoffs(); const stored = this.requireHandoff(handoffId); stored.status = status; stored.activity.push(activity(status, summary, this.now)); } - /** Host-side result seam for deterministic qualification; not a Mos consumer. */ + /** Host-side result seam for deterministic qualification; not an orchestrator consumer. */ recordResult(handoffId: string, status: 'completed' | 'failed', summary: string): void { this.pruneExpiredHandoffs(); const stored = this.requireHandoff(handoffId); @@ -110,7 +110,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort { }; } - private receipt(handoff: MosHandoff, status: MosHandoffStatus): MosHandoffReceipt { + private receipt(handoff: Handoff, status: HandoffStatus): HandoffReceipt { return { handoffId: handoff.handoffId, targetAgentId: handoff.targetAgentId, @@ -141,7 +141,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort { stored.handoff.scope.actorId !== scope.actorId || stored.handoff.scope.requesterAgentId !== scope.requesterAgentId ) { - throw new InMemoryMosCoordinationError('forbidden', 'Handoff scope does not match'); + throw new InMemoryInteractionCoordinationError('forbidden', 'Handoff scope does not match'); } return stored; } @@ -149,12 +149,12 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort { private requireHandoff(handoffId: string): StoredHandoff { const stored = this.handoffs.get(handoffId); if (stored === undefined) { - throw new InMemoryMosCoordinationError('not_found', 'Handoff was not found'); + throw new InMemoryInteractionCoordinationError('not_found', 'Handoff was not found'); } return stored; } - private assertSameHandoff(existing: MosHandoff, incoming: MosHandoff): void { + private assertSameHandoff(existing: Handoff, incoming: Handoff): void { if ( existing.targetAgentId !== incoming.targetAgentId || existing.request.idempotencyKey !== incoming.request.idempotencyKey || @@ -166,7 +166,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort { existing.scope.correlationId !== incoming.scope.correlationId || existing.scope.requesterAgentId !== incoming.scope.requesterAgentId ) { - throw new InMemoryMosCoordinationError( + throw new InMemoryInteractionCoordinationError( 'conflict', 'Handoff ID is already bound to different immutable input', ); @@ -174,31 +174,31 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort { } } -export type InMemoryMosCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found'; +export type InMemoryInteractionCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found'; -export class InMemoryMosCoordinationError extends Error { +export class InMemoryInteractionCoordinationError extends Error { constructor( - readonly code: InMemoryMosCoordinationErrorCode, + readonly code: InMemoryInteractionCoordinationErrorCode, message: string, ) { super(message); - this.name = InMemoryMosCoordinationError.name; + this.name = InMemoryInteractionCoordinationError.name; } } function activity( - status: MosHandoffStatus, + status: HandoffStatus, summary: string, now: () => Date, -): MosCoordinationActivity { +): InteractionCoordinationActivity { return { occurredAt: now().toISOString(), status, summary }; } -function copyActivity(entry: MosCoordinationActivity): MosCoordinationActivity { +function copyActivity(entry: InteractionCoordinationActivity): InteractionCoordinationActivity { return { ...entry }; } -function snapshotHandoff(handoff: MosHandoff): MosHandoff { +function snapshotHandoff(handoff: Handoff): Handoff { return Object.freeze({ handoffId: handoff.handoffId, targetAgentId: handoff.targetAgentId, diff --git a/packages/coord/src/index.ts b/packages/coord/src/index.ts index cf59bac..db70874 100644 --- a/packages/coord/src/index.ts +++ b/packages/coord/src/index.ts @@ -3,22 +3,25 @@ export { parseTasksFile, updateTaskStatus, writeTasksFile } from './tasks-file.j export { runTask, resumeTask } from './runner.js'; export { getMissionStatus, getTaskStatus } from './status.js'; export { - InMemoryMosCoordinationError, - InMemoryMosCoordinationPort, -} from './in-memory-mos-coordination-port.js'; -export { MosCoordinationAuthorityError, MosCoordinationClient } from './mos-coordination.js'; + InMemoryInteractionCoordinationError, + InMemoryInteractionCoordinationPort, +} from './in-memory-interaction-coordination-port.js'; +export { + InteractionCoordinationAuthorityError, + InteractionCoordinationClient, +} from './interaction-coordination.js'; export type { CoordinationObservation, CoordinationResult, CoordinationScope, - MosCoordinationActivity, - MosCoordinationIdentity, - MosCoordinationPort, - MosHandoff, - MosHandoffReceipt, - MosHandoffRequest, - MosHandoffStatus, -} from './mos-coordination.js'; + InteractionCoordinationActivity, + InteractionCoordinationIdentity, + InteractionCoordinationPort, + Handoff, + HandoffReceipt, + HandoffRequest, + HandoffStatus, +} from './interaction-coordination.js'; export type { CreateMissionOptions, Mission, diff --git a/packages/coord/src/mos-coordination.ts b/packages/coord/src/interaction-coordination.ts similarity index 66% rename from packages/coord/src/mos-coordination.ts rename to packages/coord/src/interaction-coordination.ts index f71d2ea..a07a3ff 100644 --- a/packages/coord/src/mos-coordination.ts +++ b/packages/coord/src/interaction-coordination.ts @@ -1,4 +1,4 @@ -export type MosHandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed'; +export type HandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed'; export interface CoordinationScope { readonly actorId: string; @@ -8,44 +8,44 @@ export interface CoordinationScope { readonly requesterAgentId: string; } -export interface MosCoordinationIdentity { +export interface InteractionCoordinationIdentity { readonly interactionAgentId: string; readonly orchestrationAgentId: string; } -export interface MosHandoffRequest { +export interface HandoffRequest { readonly idempotencyKey: string; readonly summary: string; readonly context?: string; readonly missionId?: string; } -export interface MosHandoff { +export interface Handoff { readonly handoffId: string; readonly targetAgentId: string; - readonly request: MosHandoffRequest; + readonly request: HandoffRequest; readonly scope: CoordinationScope; } -export interface MosHandoffReceipt { +export interface HandoffReceipt { readonly handoffId: string; readonly targetAgentId: string; readonly status: 'queued' | 'accepted'; readonly correlationId: string; } -export interface MosCoordinationActivity { +export interface InteractionCoordinationActivity { readonly occurredAt: string; - readonly status: MosHandoffStatus; + readonly status: HandoffStatus; readonly summary: string; } export interface CoordinationObservation { readonly handoffId: string; readonly targetAgentId: string; - readonly status: MosHandoffStatus; + readonly status: HandoffStatus; readonly correlationId: string; - readonly activity: readonly MosCoordinationActivity[]; + readonly activity: readonly InteractionCoordinationActivity[]; } export interface CoordinationResult { @@ -61,25 +61,25 @@ export interface CoordinationResult { * its progress/result, but it cannot issue worker, review, merge, or other * general orchestration commands. */ -export interface MosCoordinationPort { - handoff(handoff: MosHandoff): Promise; +export interface InteractionCoordinationPort { + handoff(handoff: Handoff): Promise; observe(handoffId: string, scope: CoordinationScope): Promise; result(handoffId: string, scope: CoordinationScope): Promise; } -export type MosCoordinationAuthorityErrorCode = +export type InteractionCoordinationAuthorityErrorCode = | 'invalid_identity' | 'requester_forbidden' | 'target_drift' | 'correlation_drift'; -export class MosCoordinationAuthorityError extends Error { +export class InteractionCoordinationAuthorityError extends Error { constructor( - readonly code: MosCoordinationAuthorityErrorCode, + readonly code: InteractionCoordinationAuthorityErrorCode, message: string, ) { super(message); - this.name = MosCoordinationAuthorityError.name; + this.name = InteractionCoordinationAuthorityError.name; } } @@ -87,20 +87,20 @@ export class MosCoordinationAuthorityError extends Error { * Enforces the interaction-to-orchestration authority boundary before a * transport is reached. Identity names remain configuration data. */ -export class MosCoordinationClient { - private readonly identity: MosCoordinationIdentity; +export class InteractionCoordinationClient { + private readonly identity: InteractionCoordinationIdentity; constructor( - identity: MosCoordinationIdentity, - private readonly port: MosCoordinationPort, + identity: InteractionCoordinationIdentity, + private readonly port: InteractionCoordinationPort, private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(), ) { this.identity = normalizeIdentity(identity); } - async handoff(request: MosHandoffRequest, scope: CoordinationScope): Promise { + async handoff(request: HandoffRequest, scope: CoordinationScope): Promise { this.assertRequester(scope); - const handoff: MosHandoff = { + const handoff: Handoff = { handoffId: this.handoffIdFactory(), targetAgentId: this.identity.orchestrationAgentId, request: snapshotRequest(request), @@ -111,15 +111,15 @@ export class MosCoordinationClient { receipt.handoffId !== handoff.handoffId || receipt.targetAgentId !== handoff.targetAgentId ) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'target_drift', - 'Mos coordination transport returned a mismatched handoff target', + 'Interaction coordination transport returned a mismatched handoff target', ); } if (receipt.correlationId !== handoff.scope.correlationId) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'correlation_drift', - 'Mos coordination transport returned a mismatched correlation ID', + 'Interaction coordination transport returned a mismatched correlation ID', ); } return receipt; @@ -137,7 +137,7 @@ export class MosCoordinationClient { private assertRequester(scope: CoordinationScope): void { if (scope.requesterAgentId !== this.identity.interactionAgentId) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'requester_forbidden', 'Requester is not the configured interaction agent', ); @@ -149,15 +149,15 @@ export class MosCoordinationClient { scope: CoordinationScope, ): CoordinationObservation { if (observation.targetAgentId !== this.identity.orchestrationAgentId) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'target_drift', - 'Mos coordination transport returned an unexpected observation target', + 'Interaction coordination transport returned an unexpected observation target', ); } if (observation.correlationId !== scope.correlationId) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'correlation_drift', - 'Mos coordination transport returned a mismatched observation correlation ID', + 'Interaction coordination transport returned a mismatched observation correlation ID', ); } return observation; @@ -165,32 +165,34 @@ export class MosCoordinationClient { private assertResult(result: CoordinationResult, scope: CoordinationScope): CoordinationResult { if (result.targetAgentId !== this.identity.orchestrationAgentId) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'target_drift', - 'Mos coordination transport returned an unexpected result target', + 'Interaction coordination transport returned an unexpected result target', ); } if (result.correlationId !== scope.correlationId) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'correlation_drift', - 'Mos coordination transport returned a mismatched result correlation ID', + 'Interaction coordination transport returned a mismatched result correlation ID', ); } return result; } } -function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationIdentity { +function normalizeIdentity( + identity: InteractionCoordinationIdentity, +): InteractionCoordinationIdentity { const interactionAgentId = identity.interactionAgentId.trim(); const orchestrationAgentId = identity.orchestrationAgentId.trim(); if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'invalid_identity', 'Interaction and orchestration identities are required', ); } if (interactionAgentId === orchestrationAgentId) { - throw new MosCoordinationAuthorityError( + throw new InteractionCoordinationAuthorityError( 'invalid_identity', 'Interaction and orchestration identities must differ', ); @@ -198,7 +200,7 @@ function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationId return Object.freeze({ interactionAgentId, orchestrationAgentId }); } -function snapshotRequest(request: MosHandoffRequest): MosHandoffRequest { +function snapshotRequest(request: HandoffRequest): HandoffRequest { return Object.freeze({ ...request }); } diff --git a/packages/mosaic/framework/fleet/roles/operator-interaction.md b/packages/mosaic/framework/fleet/roles/operator-interaction.md index 48f2490..81f40bb 100644 --- a/packages/mosaic/framework/fleet/roles/operator-interaction.md +++ b/packages/mosaic/framework/fleet/roles/operator-interaction.md @@ -2,10 +2,10 @@ The **operator-interaction** role is the authorized human interaction plane for Mosaic. It presents runtime and fleet state, mediates approved actions, and -hands coding or general orchestration work to Mos. +hands coding or general orchestration work to the orchestrator. ## Boundaries -- It does not claim Mos-owned coding or general orchestration work. +- It does not claim orchestrator-owned coding or general orchestration work. - It exposes only the configured, observable tool policy. - It does not receive or surface credentials in its effective policy. diff --git a/packages/mosaic/framework/fleet/roster.schema.json b/packages/mosaic/framework/fleet/roster.schema.json index 85b862c..386ac9e 100644 --- a/packages/mosaic/framework/fleet/roster.schema.json +++ b/packages/mosaic/framework/fleet/roster.schema.json @@ -75,6 +75,14 @@ "type": "string", "pattern": "^[A-Za-z0-9_.-]+$" }, + "alias": { + "description": "Optional operator-defined display name for the agent.", + "type": "string" + }, + "provider": { + "description": "Optional agent runtime provider identifier such as openai-codex.", + "type": "string" + }, "runtime": { "type": "string" }, diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index 641c060..0c2fcbe 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -189,6 +189,36 @@ describe('fleet roster parsing', () => { expect(getRosterAgent(roster, 'canary-pi').runtime).toBe('pi'); }); + it('accepts optional agent alias and provider metadata without requiring them', async () => { + cleanup = await tempDir(); + const rosterPath = join(cleanup, 'roster.yaml'); + await writeFile( + rosterPath, + [ + 'version: 1', + 'transport: tmux', + 'agents:', + ' - name: interaction', + ' alias: Friday', + ' provider: openai-codex', + ' runtime: pi', + ' - name: worker0', + ' runtime: codex', + ].join('\n'), + ); + + const roster = await loadFleetRoster(rosterPath); + + expect(getRosterAgent(roster, 'interaction')).toMatchObject({ + name: 'interaction', + alias: 'Friday', + provider: 'openai-codex', + }); + expect(getRosterAgent(roster, 'worker0')).toMatchObject({ name: 'worker0' }); + expect(getRosterAgent(roster, 'worker0').alias).toBeUndefined(); + expect(getRosterAgent(roster, 'worker0').provider).toBeUndefined(); + }); + it('socketArgs: named socket → -L ; empty → no -L (default socket)', () => { expect(socketArgs('mosaic-fleet')).toEqual(['-L', 'mosaic-fleet']); expect(socketArgs('')).toEqual([]); @@ -1872,7 +1902,61 @@ describe('fleet ps — tenant and host', () => { }); }); -describe('fleet ps — JSON output shape (FR-6)', () => { +describe('fleet ps — aliases and JSON output shape (FR-6)', () => { + it('shows an agent alias in table output and falls back to name when absent', async () => { + const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-')); + const rosterPath = join(home, 'fleet', 'roster.yaml'); + await mkdir(join(home, 'fleet'), { recursive: true }); + await writeFile( + rosterPath, + [ + 'version: 1', + 'transport: tmux', + 'agents:', + ' - name: interaction', + ' alias: Friday', + ' runtime: pi', + ' - name: worker0', + ' runtime: codex', + ].join('\n'), + ); + + const runner: CommandRunner = async (command, args) => { + const fullArgs = [command, ...args].join(' '); + if (fullArgs.includes('list-sessions')) { + return { stdout: 'interaction\nworker0\n', stderr: '', exitCode: 0 }; + } + return { + stdout: fullArgs.includes('systemctl') + ? 'ActiveState=active\nSubState=running\nUnitFileState=enabled\n' + : '12345 pi 0 0 0 0\n', + stderr: '', + exitCode: 0, + }; + }; + + const lines: string[] = []; + const origLog = console.log; + console.log = (msg: string) => { + lines.push(msg); + }; + + const program = new Command(); + program.exitOverride(); + registerFleetCommand(program, { runner, mosaicHome: home }); + + try { + await program.parseAsync(['node', 'mosaic', 'fleet', 'ps']); + } finally { + console.log = origLog; + await rm(home, { recursive: true, force: true }); + } + + const output = lines.join('\n'); + expect(output).toContain('Friday'); + expect(output).toContain('worker0'); + }); + it('produces --json records including tenant_id and host for each agent', async () => { const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-')); const rosterPath = join(home, 'fleet', 'roster.yaml'); diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index 293fc9b..87fdc9d 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -81,6 +81,8 @@ interface RawFleetRoster { runtimes?: Record; agents?: Array<{ name?: unknown; + alias?: unknown; + provider?: unknown; runtime?: unknown; class?: unknown; working_directory?: unknown; @@ -102,6 +104,8 @@ interface RawFleetRoster { export interface FleetAgent { name: string; + alias?: string; + provider?: string; runtime: string; className: string; workingDirectory?: string; @@ -1074,6 +1078,7 @@ export interface HeartbeatInfo { export interface AgentPsRow { name: string; + alias?: string; tenant_id: string; host: string; runtime: string; @@ -1755,6 +1760,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = rows.push({ name: agent.name, + alias: agent.alias, tenant_id, host, runtime: agent.runtime, @@ -1888,7 +1894,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = console.log( [ - row.name.padEnd(18), + (row.alias ?? row.name).padEnd(18), row.tenant_id.padEnd(12), row.host.padEnd(12), row.runtime.padEnd(10), @@ -2494,6 +2500,8 @@ function normalizeAgent(raw: NonNullable[number]): Fle assertObject(raw, 'Fleet roster agent'); assertKnownKeys(raw, 'Fleet roster agent', [ 'name', + 'alias', + 'provider', 'runtime', 'class', 'working_directory', @@ -2525,6 +2533,8 @@ function normalizeAgent(raw: NonNullable[number]): Fle } return { name, + alias: optionalString(raw.alias, `Fleet roster agent "${name}" alias`), + provider: optionalString(raw.provider, `Fleet roster agent "${name}" provider`), runtime, className: stringValue(raw.class, 'worker', `Fleet roster agent "${name}" class`), workingDirectory: optionalString( @@ -2827,6 +2837,12 @@ export function serializeRosterToYaml(roster: FleetRoster): string { runtime: agent.runtime, class: agent.className, }; + if (agent.alias !== undefined) { + raw['alias'] = agent.alias; + } + if (agent.provider !== undefined) { + raw['provider'] = agent.provider; + } if (agent.workingDirectory !== undefined) { raw['working_directory'] = agent.workingDirectory; } diff --git a/packages/mosaic/src/commands/launch.ts b/packages/mosaic/src/commands/launch.ts index 4d96f9a..03a0d84 100644 --- a/packages/mosaic/src/commands/launch.ts +++ b/packages/mosaic/src/commands/launch.ts @@ -414,7 +414,7 @@ function readFleetToolPolicyBlock(policy: string | undefined): string { '', 'Permitted: authorized conversation, status, retrieval, and safe diagnostics.', 'Denied by default: coding/general orchestration claims, direct fleet control, destructive actions, and credential access.', - 'Delegate Mos-owned work through the authorized handoff boundary.', + 'Delegate orchestrator-owned work through the authorized handoff boundary.', ].join('\n'); }