Three independently ugly bugs that all fed the same log flood:
* Electron's `WebFrameMain.send` writes every "Render frame was disposed"
to stderr via `console.error`. When the worker fans a second
agent-event past a reloading renderer, we produced thousands of
identical lines and stalled the main loop on stderr I/O. Added a
`mainFrame.detached` guard so the send is short-circuited before
Electron's own catch block fires, and installed a targeted filter in
`suppress-warnings.ts` (now actually imported from the entry point)
so the specific `console.error("Error sending from webFrameMain:"…)`
noise never reaches the terminal. Any other payload still surfaces.
* `dispatchAgentMessage` accepted the human-reader sentinel as a wake
target when an agent replied to `__board__` — `wakeAgentIfPossible`
then called `db.getAgent("__board__")`, threw, and the dispatcher
kept repeating on every send. Strip `HUMAN_SENDER_ID` at
`resolveMessageWakeTargets`, and keep a defense-in-depth guard in the
heartbeat so a stray caller (future automation fan-out) cannot
reintroduce the crash.
* Pi's skills loader rejects any SKILL.md whose frontmatter `name`
diverges from its parent directory. The four bundled `my-writing-style`
copies declared `name: writing-style-skill`, so every install emitted
a warning and dropped the skill. They also lived as orphan gitlinks
(mode 160000 without .gitmodules), and the global `*.md` gitignore
hid every other SKILL.md from version control. Fixed the frontmatter
to match the dir, converted the dangling submodules to plain trees,
and allowlisted `builtin-skills/**/*.md` so a fresh clone carries the
catalogue instead of silently losing it.
133 lines
3.5 KiB
Markdown
133 lines
3.5 KiB
Markdown
---
|
|
title: Web Framework Concerns Stay in Interface Layer
|
|
impact: MEDIUM
|
|
impactDescription: enables delivery mechanism substitution
|
|
tags: frame, web, http, interface
|
|
---
|
|
|
|
## Web Framework Concerns Stay in Interface Layer
|
|
|
|
HTTP-specific code (requests, responses, headers, cookies, sessions) belongs in the interface adapters layer. Use cases should be callable from any delivery mechanism.
|
|
|
|
**Incorrect (use case coupled to HTTP):**
|
|
|
|
```typescript
|
|
// application/usecases/LoginUseCase.ts
|
|
import { Request, Response } from 'express'
|
|
import { sign } from 'jsonwebtoken'
|
|
|
|
export class LoginUseCase {
|
|
async execute(req: Request, res: Response) {
|
|
const { email, password } = req.body
|
|
|
|
const user = await this.users.findByEmail(email)
|
|
if (!user || !user.verifyPassword(password)) {
|
|
return res.status(401).json({ error: 'Invalid credentials' })
|
|
}
|
|
|
|
// Set HTTP-only cookie
|
|
const token = sign({ userId: user.id }, process.env.JWT_SECRET)
|
|
res.cookie('auth_token', token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict'
|
|
})
|
|
|
|
return res.json({ user: { id: user.id, email: user.email } })
|
|
}
|
|
}
|
|
|
|
// Cannot call from CLI, message queue, or test without Express
|
|
```
|
|
|
|
**Correct (use case independent of delivery):**
|
|
|
|
```typescript
|
|
// application/usecases/LoginUseCase.ts
|
|
export interface LoginCommand {
|
|
email: string
|
|
password: string
|
|
}
|
|
|
|
export interface LoginResult {
|
|
userId: string
|
|
email: string
|
|
authToken: string
|
|
}
|
|
|
|
export class LoginUseCase {
|
|
constructor(
|
|
private users: UserRepository,
|
|
private tokenService: TokenService
|
|
) {}
|
|
|
|
async execute(command: LoginCommand): Promise<LoginResult> {
|
|
const user = await this.users.findByEmail(command.email)
|
|
|
|
if (!user) {
|
|
throw new InvalidCredentialsError()
|
|
}
|
|
|
|
if (!user.verifyPassword(command.password)) {
|
|
throw new InvalidCredentialsError()
|
|
}
|
|
|
|
const token = this.tokenService.generate({ userId: user.id.value })
|
|
|
|
return {
|
|
userId: user.id.value,
|
|
email: user.email.value,
|
|
authToken: token
|
|
}
|
|
}
|
|
}
|
|
|
|
// interface/http/AuthController.ts
|
|
import { Request, Response } from 'express'
|
|
|
|
export class AuthController {
|
|
constructor(private loginUseCase: LoginUseCase) {}
|
|
|
|
async login(req: Request, res: Response) {
|
|
try {
|
|
const result = await this.loginUseCase.execute({
|
|
email: req.body.email,
|
|
password: req.body.password
|
|
})
|
|
|
|
// HTTP concerns in controller
|
|
res.cookie('auth_token', result.authToken, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict'
|
|
})
|
|
|
|
return res.json({
|
|
user: { id: result.userId, email: result.email }
|
|
})
|
|
} catch (error) {
|
|
if (error instanceof InvalidCredentialsError) {
|
|
return res.status(401).json({ error: 'Invalid credentials' })
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// interface/cli/AuthCli.ts - Same use case, different delivery
|
|
export class AuthCli {
|
|
async login(email: string, password: string) {
|
|
const result = await this.loginUseCase.execute({ email, password })
|
|
console.log(`Logged in as ${result.email}`)
|
|
fs.writeFileSync('.auth_token', result.authToken)
|
|
}
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Same use case for HTTP, CLI, WebSocket, queue consumers
|
|
- Easy to test without HTTP mocking
|
|
- Web framework upgrades isolated to interface layer
|
|
|
|
Reference: [Clean Architecture - The Web is a Detail](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch31.xhtml)
|