diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df15eaf..4c2b1688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ new version heading in the same commit. ## [Unreleased] +## [0.447.2] - 2026-09-18 +### Fixed +- **An app review card no longer outlives the review.** The Apps console acts on the app, never on the + card, so nothing ever closed an `app.proposed` card: an app published weeks ago still read "awaiting + review" in Needs you, with no button to clear it (live: five cards on one tenant, the oldest two months + old — the same bug the skills lane fixed in v0.404.1). Publishing an app now closes its card, deleting + one closes it too, and a boot sweep heals cards the old routes left behind, re-deriving each from the + apps store — published reads as approved, gone as rejected. A card whose app is still sitting + unpublished is left open, because that one is genuinely pending; unpublishing doesn't close a card + either, since an agent's edit to a live app unpublishes it precisely to ask for a re-review. Every + review card also gained a **Dismiss** button — the manual escape hatch for one whose subject was + handled outside the console (a per-member hide, exactly what "dismiss all" already did in bulk; the + proposal itself stays open on its own page for everyone else). + **For users:** Publishing or deleting an app now clears its "awaiting review" card from your Inbox, old + stragglers are cleaned up on the next restart, and any review card can be dismissed on the spot. + [Open Inbox](#/inbox) + ## [0.447.1] - 2026-09-17 ### Fixed - **The unattended-run brief no longer tells agents three things that stopped being true.** It said diff --git a/package-lock.json b/package-lock.json index 79953037..af6272a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.447.1", + "version": "0.447.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.447.1", + "version": "0.447.2", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index b3aa4fe9..de693c6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.447.1", + "version": "0.447.2", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", @@ -27,7 +27,7 @@ "check-deps": "bash scripts/install-deps.sh --check", "dev": "ts-node src/cli.ts serve", "demo:dev": "ts-node src/demo.ts", - "test:governance": "node scripts/version-sync-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs", + "test:governance": "node scripts/version-sync-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs && node scripts/app-card-close-test.cjs", "test:alert-staleness": "node scripts/alert-staleness-test.cjs", "test:deps": "node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-login-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs", "test:dm-continuity": "node scripts/dm-continuity-test.cjs", diff --git a/scripts/app-card-close-test.cjs b/scripts/app-card-close-test.cjs new file mode 100644 index 00000000..ef060394 --- /dev/null +++ b/scripts/app-card-close-test.cjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +/* An `app.proposed` review card must not outlive the review. + * + * The Apps routes act on the APP (publish / delete), never on the card, so before this fix nothing ever + * closed one: a card whose app had been live for weeks still read "awaiting review" in Needs you — five + * of them on one live tenant, the oldest two months old. Pins the three ways out: + * 1. publishing the app closes its card (publishing IS what the card asks for); + * 2. deleting the app closes it too — it can never be acted on again; + * 3. UNPUBLISHING does not: an agent's edit to a live app unpublishes it and raises a re-review card, + * and that card is still waiting for a human; + * 4. the boot sweep heals cards left open by the old routes, and leaves a genuinely pending one alone. + */ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'aos-app-card-test-')); +process.env.AGENT_OS_HOME = HOME; +process.env.AGENT_OS_TENANT = 'testco'; +process.env.AOS_NO_TTYD = '1'; + +let pass = 0, fail = 0; +const assert = (c, name, d) => c ? (pass++, console.log(` \x1b[32m✓\x1b[0m ${name}`)) : (fail++, console.log(` \x1b[31m✗ ${name}\x1b[0m${d !== undefined ? ' — ' + JSON.stringify(d).slice(0, 400) : ''}`)); + +(async () => { + const { createHttpServer } = require(path.join(ROOT, 'dist/server.js')); + const { TenantRegistry } = require(path.join(ROOT, 'dist/tenant-registry.js')); + const registry = new TenantRegistry(ROOT, 0, path.join(ROOT, 'config/agent-os.config.json')); + registry.bootAll(); + const { os: aos, tm } = registry.default(); + const server = createHttpServer(registry); + await new Promise((r) => server.listen(0, '127.0.0.1', r)); + const base = `http://127.0.0.1:${server.address().port}`; + + const owner = aos.team.listMembers().find((m) => m.role === 'owner'); + const cookie = `aos_sid=${aos.team.createSession(owner.id)}`; + const get = async (p) => (await fetch(base + p, { headers: { cookie } })).json(); + const post = async (p) => (await fetch(base + p, { method: 'POST', headers: { cookie, 'content-type': 'application/json' }, body: '{}' })).json(); + const del = async (p) => (await fetch(base + p, { method: 'DELETE', headers: { cookie } })).json(); + + // The NEWEST card for a slug, straight from the row — `status` is what "Needs you" keys on. Newest + // because one app raises several cards over its life (proposed, then re-review after each agent edit). + const cardStatus = (slug) => { + const rows = aos.db.prepare(`SELECT args, status FROM messages WHERE type = 'app.proposed' ORDER BY created_at DESC, id DESC`).all(); + const row = rows.find((r) => { try { return JSON.parse(r.args || '{}').slug === slug; } catch { return false; } }); + return row ? row.status : null; + }; + const propose = (slug) => { + aos.apps.scaffold(slug, { name: slug, createdBy: 'agent:app-builder' }); + tm.postAppCard({ slug, agent: 'app-builder', title: `App proposed — ${slug}`, body: `app-builder built "${slug}". Review it and publish it to make it live.` }); + }; + + console.log('\n\x1b[1m1) publishing the app closes its card\x1b[0m'); + propose('voice-notes'); + assert(cardStatus('voice-notes') === 'open', 'the card starts open — it is the review ask', cardStatus('voice-notes')); + const inbox = await get('/api/messages'); + assert(inbox.some((m) => m.type === 'app.proposed' && m.args?.slug === 'voice-notes'), 'and an owner sees it in the inbox feed'); + const pub = await post('/api/apps/voice-notes/publish'); + assert(pub.ok === true && pub.app.published === true, 'publish succeeds', pub.error ?? pub.app?.published); + assert(cardStatus('voice-notes') === 'approved', 'the card is resolved by the act it was asking for', cardStatus('voice-notes')); + + console.log('\n\x1b[1m2) unpublishing does NOT — a re-review card is still waiting\x1b[0m'); + tm.postAppCard({ slug: 'voice-notes', agent: 'app-builder', title: 'App edited — voice-notes', body: 'app-builder changed the live app; re-publish to make the change live.' }); + const unpub = await post('/api/apps/voice-notes/unpublish'); + assert(unpub.ok === true && unpub.app.published === false, 'unpublish succeeds', unpub.error); + assert(cardStatus('voice-notes') === 'open', 'the re-review card stays open — nobody has reviewed the edit yet', cardStatus('voice-notes')); + await post('/api/apps/voice-notes/publish'); + assert(cardStatus('voice-notes') === 'approved', 're-publishing closes it', cardStatus('voice-notes')); + + console.log('\n\x1b[1m3) deleting the app closes its card too\x1b[0m'); + propose('scratch-app'); + const gone = await del('/api/apps/scratch-app'); + assert(gone.ok === true, 'delete succeeds', gone.error); + assert(cardStatus('scratch-app') === 'rejected', 'the card cannot be acted on again, so it is closed', cardStatus('scratch-app')); + + console.log('\n\x1b[1m4) the boot sweep heals cards the old routes left behind\x1b[0m'); + // Exactly the live shape: three cards, whose apps were published / deleted / left proposed outside the + // console, all still marked open — what a tenant upgrading into this fix actually carries. + propose('stale-published'); await post('/api/apps/stale-published/publish'); + propose('stale-deleted'); await del('/api/apps/stale-deleted'); + propose('still-pending'); + aos.db.prepare(`UPDATE messages SET status = 'open' WHERE type = 'app.proposed'`).run(); + server.close(); + await registry.stopAll?.(); + + const registry2 = new TenantRegistry(ROOT, 0, path.join(ROOT, 'config/agent-os.config.json')); + registry2.bootAll(); + const aos2 = registry2.default().os; + const status2 = (slug) => { + const rows = aos2.db.prepare(`SELECT args, status FROM messages WHERE type = 'app.proposed' ORDER BY created_at DESC, id DESC`).all(); + const row = rows.find((r) => { try { return JSON.parse(r.args || '{}').slug === slug; } catch { return false; } }); + return row ? row.status : null; + }; + assert(status2('stale-published') === 'approved', 'a card whose app is live reads as approved', status2('stale-published')); + assert(status2('stale-deleted') === 'rejected', 'a card whose app is gone reads as rejected', status2('stale-deleted')); + assert(status2('still-pending') === 'open', 'a card whose app is still sitting unpublished is left alone — that one IS pending', status2('still-pending')); + await registry2.stopAll?.(); + + fs.rmSync(HOME, { recursive: true, force: true }); + console.log(`\n${fail ? '\x1b[31m' : '\x1b[32m'}${pass} passed, ${fail} failed\x1b[0m`); + process.exit(fail ? 1 : 0); +})(); diff --git a/src/server.ts b/src/server.ts index fbfa799a..847cd38b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6664,6 +6664,7 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req: if (method === 'DELETE') { appSup?.kill(slug, 'deleted'); os.apps.remove(slug); + tm.resolveAppCards(slug, 'rejected'); // the app is gone — its review card can never be acted on if ((app.domains ?? []).length) currentRegistry?.invalidateAppDomains(); os.audit.append({ ts: Date.now(), runId: '-', tenant: os.tenant, principal: me.email, type: 'app.deleted', data: { app: slug, by: me.email } }); return sendJson(res, 200, { ok: true }); @@ -6743,6 +6744,11 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req: const published = action === 'publish'; os.apps.setPublished(slug, published); if (!published) appSup?.kill(slug, 'unpublished'); + // Publishing IS the card's ask ("review its code + capabilities and publish it"), so close the review + // card here — the console acts on the app, never on the card, and without this it sits in Needs you + // forever. Unpublishing deliberately does NOT close one: an agent's edit to a live app unpublishes it + // and raises a re-review card, and that card is still waiting. + if (published) tm.resolveAppCards(slug, 'approved'); // Publish state gates which domains are live (only published apps route by Host) → refresh the index. if ((os.apps.get(slug)?.domains ?? []).length) currentRegistry?.invalidateAppDomains(); os.audit.append({ ts: Date.now(), runId: '-', tenant: os.tenant, principal: me.email, type: published ? 'app.published' : 'app.unpublished', data: { app: slug, by: me.email } }); diff --git a/src/terminal.ts b/src/terminal.ts index 6cf1b20f..7a260003 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -1069,6 +1069,7 @@ export class TerminalManager { this.refreshTranscriptRoots(); this.sweepLaunchMarkers(); this.sweepStaleSkillProposals(); + this.sweepStaleAppProposals(); } /** One-shot boot heal for review cards left open by the pre-v0.404.1 skills routes, which resolved the @@ -1103,6 +1104,33 @@ export class TerminalManager { } catch { /* advisory — never block boot on a heal */ } } + /** The same one-shot boot heal for 'app.proposed' cards, which had the identical bug: the Apps routes + * published/deleted the APP but never closed its card, so a review finished weeks ago still reads + * "awaiting review" in Needs you (live: five on one tenant, the oldest two months old). Re-derives + * each open card from the apps store — published reads as approved, gone as rejected — and leaves a + * card whose app is still sitting there unpublished alone, because that one IS genuinely pending. */ + private sweepStaleAppProposals(): void { + try { + const rows = this.db + .prepare(`SELECT id, args FROM messages WHERE type = 'app.proposed' AND status = 'open'`) + .all<{ id: string; args: string | null }>(); + if (!rows.length) return; + const upd = this.db.prepare(`UPDATE messages SET status = ? WHERE id = ?`); + let healed = 0; + for (const r of rows) { + let a: Record = {}; + try { a = r.args ? JSON.parse(r.args) : {}; } catch { continue; } + const slug = String(a.slug ?? ''); + if (!slug) continue; + const app = this.os.apps.get(slug); + const status = !app ? 'rejected' : app.published ? 'approved' : undefined; + if (!status) continue; + upd.run(status, r.id); healed++; + } + if (healed) this.audit('-', 'system', 'app.proposals.healed', { count: healed }); + } catch { /* advisory — never block boot on a heal */ } + } + /** Teach the transcript reader where rotated sessions wrote their conversations. Without it the console's * conversation view (and the transcript fallback in `detectUsageLimit`) only ever sees the SERVER's own * `~/.claude/projects`, so every run made under a pooled account reads back as "no transcript". Called @@ -7370,6 +7398,25 @@ export class TerminalManager { return closed; } + /** Mark the open 'app.proposed' review card(s) for a hosted app resolved once a human acted on it — + * published it (the card's whole ask) or deleted it. Found by payload (`args.slug`) for the same + * reason as {@link resolveSkillProposals}: the Apps console acts on the APP, never on the card. + * Returns how many cards were closed. */ + resolveAppCards(slug: string, status: 'approved' | 'rejected'): number { + const rows = this.db + .prepare(`SELECT id, args FROM messages WHERE type = 'app.proposed' AND status = 'open'`) + .all<{ id: string; args: string | null }>(); + const upd = this.db.prepare(`UPDATE messages SET status = ? WHERE id = ? AND type = 'app.proposed'`); + let closed = 0; + for (const r of rows) { + let a: Record = {}; + try { a = r.args ? JSON.parse(r.args) : {}; } catch { /* tolerate a corrupt payload */ } + if (String(a.slug ?? '') !== slug) continue; + upd.run(status, r.id); closed++; + } + return closed; + } + /** Open (unresolved) skill.request cards — the Skills page's agent-request review section. */ openSkillRequests(): { id: string; skill: string; source: string; agent: string; rationale?: string; createdAt: number }[] { return this.db diff --git a/web/src/App.tsx b/web/src/App.tsx index f84370d8..163bc995 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -7061,12 +7061,14 @@ function ActionItem({ m, me, members, agents, onOpen, onDismiss }: { m: Msg; me: - {/* An OS notice, not an agent's proposal: there is nothing to approve or reject, so without this - a human who has already fixed the problem has no way to clear the card. It also self-heals - server-side on the next connection refresh — this is the manual escape hatch. */} - {m.type === 'connection.expired' && ( - - )} + {/* Every review card needs a way out. Each one is resolved on its own page, not here, so a card + whose subject was already handled elsewhere (or outside the console entirely) would otherwise + sit in "Needs you" forever — which is exactly what happened to app/skill proposal cards for + months. The routes now close their own cards, and this is the manual escape hatch for the + rest. It only hides the card for THIS member (`message_state`); the proposal itself stays + open and actionable on its page, and everyone else still sees it. "dismiss all" already + cleared these in one blunt sweep — this is the per-card version of the same act. */} + {isAgentEdit && !canDecide && an owner has to approve this one} {hint && {hint}}