diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/README.md b/feature-specs/evidence/f117-feedback-2026-10-09/README.md
new file mode 100644
index 0000000000..1225cae23d
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/README.md
@@ -0,0 +1,61 @@
+# F117 restart feedback: implementation and review packet
+
+Author: 砚砚 / gpt-6.1-sol. Worktree: cat-cafe-append-receipt-feedback; branch fix/f117-append-receipt-feedback. Base d015c806d91839658788a32593735e3f4ff99254 (tree 022ebeb40ac5326109a33cac710a6550509e4c1f). This base is fork PR217's merge; its tree equals public candidate028a63af71.
+
+## Accepted user intent
+
+Canonical thread thread_msr51149hym0i79f:
+
+- 0001791549775494-000019-363c19fb and 0001791549864606-000022-3823f965: guidance/interrupt behavior is a target capability; remove transitional source-kind eligibility gates.
+- 0001791550801818-000059-bceb02e9: exactly two stop scopes, per-member and all active members; do not merge185/210/215; explain root causes and changes.
+- 0001791551501099-000083-b30df01a: investigate internal ~1second admission separately from native client startup; recover prior quote contract.
+- 0001791552770209-000113-0ebffba2, 0001791553175243-000118-fd2249d2 and 0001791553423732-000124-c2acb766: one current status/time on dot/text hover; reuse dispatch read state; missing read capability is unsupported; hover uses 投递于/读取于.
+- 0001791553220033-000119-8affed25 and 0001791553456012-000125-74f43c16: ordinary delivery should not scan Signals articles; use explicit retrieval.
+- 0001791553679904-000131-6b5fab2c and 0001791553749174-000134-3ea29b71: file the broader automatic-context audit in cat-cafe-shared, not Clowder.
+
+## Changes and why
+
+1. Queue `/continue`, durable continuation binding, and automatic exact-entry Append no longer classify eligibility by user/agent/external/plugin/system sender. Target append capability and an explicit continuation's exact parent govern transfer. Owner authorization, Queue revision/status, private/pinned exclusions, sibling targets, provider readiness, exact execution and durable source provenance remain in their existing owners. GitHub Wait216's source remains external and its typed wait metadata is retained. Queue409 displays the real failure except actual revision/entry convergence errors. There is no UI source-kind disable.
+2. Existing `LifecycleDispatchRef.inputRead` already encodes the requested three states: absent=unsupported, pending=not read, read+at=read. No duplicate consume field, store or provider inference. Console reads the unique source×target×response dispatch: 投递于 uses dispatchedAt; 读取于 uses read.at. Target theme color; only a supported pending receipt on an active run pulses. A terminal run without receipt confirmation stops animation without claiming read.
+3. Dot and text share one tooltip (name, current status/time, full source text), delayed1second on normal mouse hover. Keyboard and mobile accessibility retain existing tooltip policy. Removed browser-native title and visible 展开全文/收起 controls; a truncated row uses a hand cursor and click/Enter to expand/collapse. Jump-to-source stays separate. Existing multi-row collapse stays.
+4. Sender display names centrally omit @; route handles in body text are unchanged. Reply badge order is ↩, avatar, name: text, with flex centering. No component-specific name fallback layer.
+5. Removed duplicate “猫猫正在回复” composer banner. Current-member append guidance lives in the disposition-specific input placeholder. Reused the existing stop icon/control in the classic composer too; it cancels all exact active executions, while execution-bar member controls retain per-member scope. No queue deletion/pause or new stop protocol.
+6. Removed automatic Signals lookup dependency and article snippet injection from both routing strategies and router/bootstrap wiring. Article search/get/study/link services and stored data are untouched. Existing standalone lookup module is no longer in delivery wiring. This removes work rather than adding caches, source exceptions or moving admission labels ahead of work. Existing route timing splits entity nudges, proactive memory, disposition feedback and memory cues to distinguish remaining cost.
+
+## Validation
+
+- API271/271,17suites: real Queue/tracker/store/processor boundary, all sender kinds, stale parent fences, wait provenance, serial/parallel routes, explicit article query service, read observations, projection, entity/proactive-memory and feedback context.
+- Web127/127,13files: canonical identity across surfaces, row read/delivery state, tooltip timing, click/keyboard interaction, reply order, all-stop and queue action error behavior.
+- API TypeScript compilation exit0. Shared/API builds passed earlier; final API dist regenerated by tsc in this same worktree.
+- Full Web type check and exact base compiler-host comparison show identical single pre-existing TS2322 at proposal-card-realtime-socket.integration.test.tsx:152. No new diagnostics; not claiming full Web tsc green.
+- Browser fixture imports actual current components/styles; in-memory API and synthetic source/response data only. Desktop/mobile screenshots inspected. Checks: no duplicate banner, target dot color, one-state tooltip, expansion and Enter collapse, two actual exact cancel requests, mobile stop within viewport, no browser errors. This is not complete Next/real-provider acceptance. The temporary preview has a bounded lifetime and is not an operator runtime entry.
+- Original valid red logs retained: source gates; serial/parallel unrequested article injection; central sender labels; row interaction and one current status/time. Successful logs are compressed losslessly; input/artifact SHA256 manifest provided. Broadened tests initially lacked the local SQLite native dependency; installed the exact locked package's binding in this feature checkout and reran all271 successfully. Runtime dependencies/config untouched.
+- git diff --check passes. Formatting has no errors; existing lint warnings remain, no unsafe broad rewrite.
+
+## Performance: proved findings versus remaining diagnosis
+
+Runtime read-only metadata in internal-preparation-timings.json (only this thread’s detailed coordinates, other runs aggregate-only): idle source ingress12:26:11.035Z, admission12:26:12.091Z, queueAge1056ms; Queue preparation14.27ms, route preparation984.82ms, admission3.27ms. Subgroups memory/feedback467.75ms and identity/linked354.85ms. Later native Codex startup8.905s is separate.36samples show recurring linked-context work; this is not explained away as cold startup.
+
+Owned synthetic Signals probe: zero linked articles,500records27–28ms,2000records106–111ms. This verifies O(library-size) I/O even on no-match ordinary threads. Removing both callers removes this work from admission; no live after-deployment millisecond claim. Remaining468ms memory/feedback is not fixed by this change. Code inspection found owner-window scans and repeated registry/ledger resolution; per-operation attribution is still needed before changing memory semantics. New timing splits expose that distinction after ordinary delivery deploys, without logging message bodies or weakening privacy.
+
+Broader audit issue: https://github.com/zts212653/cat-cafe-shared/issues/8 . Initially created in the wrong repository, then transferred (no duplicate Clowder issue remains). Canonical gh readback confirms OPEN/authormindfn. Automatic tracking registration returned422 “repository does not exist or is not accessible”; no tracker was falsely claimed. Follow-up through canonical gh remains available.
+
+## PR cleanup and original capabilities
+
+Provider-verified CLOSED:210(operator explicit139),216(replaced by merged217),211/212/213/214(A2A increments absorbed),191/199(public changes already in synced main). Branches/evidence preserved.
+
+185/215 remain frozen,210closed at operator139; user explicitly prohibits merging their massive comparison diffs. Those diffs reflect their old baseline, not the capability's own size:
+
+| PR | True original slice | Capability and disposition |
+| --- | --- | --- |
+|185|11files,+1787/-1|Skip scheduled eval model wakes when prerequisite OTel evidence is unavailable; plus fork rebuild guard/registry/workflow. Gate is not in current runtime. Separate from A2A, not imported here.|
+|210|5files,+427/-23|MCP preserves raw quoted blocks and comment-first previews, keyword matching and bounded drilldown. Anchor preview part exists today; full contentBlocks/keyword projection is not fully absorbed. No giant merge.|
+|215|4files,+838/-79|gh verdict-vs-main-sync classification, fresh remote candidate/base and CLI argument binding. Corrected develop_base baseline already removed A2A dependency. No guard activation/merge here.|
+
+Historical quote contract recovered from full canonical user messages0001791430966608-000013-accfaf5b (messageId+selected fragment),0001791433389141-000027-28bf60b3 (small justified quote/read changes permitted; no scenario if/else). PR210 addresses model-facing read projection, not this reply-avatar UI or admission latency. It needs a clean current-baseline scope rather than its old large PR; no new quote storage/protocol implemented in this packet.
+
+## Risk and ownership
+
+Behavior: target-based continuation broadening, visual simplification, removal of implicit article prompt injection. Data: no new store/schema, durable inputs untouched. Security: existing owner, target, exact-parent and revision checks retained; no trust in source labels. Contract: existing dispatch receipt and cancel APIs reused; internal optional Signals router dependency removed; explicit article APIs unchanged. Irreversibility: local commit and later normal feature push/PR only; no merge, runtime code/config/process mutation or persistent data deletion.
+
+Reviewer should judge exact-source-neutral guidance with all target/provenance fences, latest tooltip semantics and two stop scopes, and whether optional article capability is now absent from normal routing. Remaining performance diagnosis is expressly open, not covered by a green test claim. Review before public push; operator will restart after the incremental delivery returns normally.
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/check-web-types.cjs b/feature-specs/evidence/f117-feedback-2026-10-09/check-web-types.cjs
new file mode 100644
index 0000000000..56e968506e
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/check-web-types.cjs
@@ -0,0 +1,49 @@
+// Compare the candidate and its exact base through a compiler host; do not
+// modify the checkout or hide any diagnostic from the full web project.
+const path = require('node:path');
+const { execFileSync } = require('node:child_process');
+const { createRequire } = require('node:module');
+const root = process.cwd();
+const web = path.join(root, 'packages/web');
+const ts = createRequire(path.join(web, 'package.json'))('typescript');
+const base = 'd015c806d91839658788a32593735e3f4ff99254';
+const changed = execFileSync('git', ['diff', '--name-only', base], { cwd: root, encoding: 'utf8' }).trim().split('\n');
+const baseline = new Map(
+ changed
+ .filter((file) => /\.[cm]?[jt]sx?$/.test(file))
+ .map((file) => [
+ path.join(root, file),
+ execFileSync('git', ['show', `${base}:${file}`], { cwd: root, encoding: 'utf8' }),
+ ]),
+);
+const configFile = ts.readConfigFile(path.join(web, 'tsconfig.json'), ts.sys.readFile);
+const config = ts.parseJsonConfigFileContent(configFile.config, ts.sys, web);
+function check(useBase) {
+ const host = ts.createCompilerHost(config.options);
+ const read = host.readFile.bind(host);
+ host.readFile = (file) => (useBase && baseline.has(file) ? baseline.get(file) : read(file));
+ const program = ts.createProgram(config.fileNames, { ...config.options, incremental: false }, host);
+ return ts.getPreEmitDiagnostics(program).map((diagnostic) => {
+ const position =
+ diagnostic.file && diagnostic.start !== undefined
+ ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
+ : undefined;
+ return {
+ file: diagnostic.file ? path.relative(root, diagnostic.file.fileName) : null,
+ line: position ? position.line + 1 : null,
+ code: diagnostic.code,
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
+ };
+ });
+}
+const candidate = check(false);
+const baseDiagnostics = check(true);
+const result = {
+ base,
+ replacedBaseFiles: baseline.size,
+ candidate,
+ baseDiagnostics,
+ sameDiagnostics: JSON.stringify(candidate) === JSON.stringify(baseDiagnostics),
+};
+console.log(JSON.stringify(result, null, 2));
+process.exitCode = result.sameDiagnostics ? 0 : 1;
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/context-audit-issue.md b/feature-specs/evidence/f117-feedback-2026-10-09/context-audit-issue.md
new file mode 100644
index 0000000000..425f09b216
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/context-audit-issue.md
@@ -0,0 +1,44 @@
+## Problem
+
+Sending an ordinary chat message performs optional capability discovery before the target can accept execution. We should audit what is automatically fetched/injected on this critical path, retain required identity/routing/session context, and make optional retrieval explicit and bounded.
+
+A confirmed example is Signals article injection. Both serial and parallel routes call `signalArticleLookup(threadId)` before model invocation. `createSignalArticleLookup` first reads all inbox records, then sequentially reads each article's study metadata to discover whether it belongs to the thread. A thread with no linked articles still traverses the library. This is Signals/news/article retrieval, not the normal message history or memory lookup.
+
+Code at the published A2A candidate `028a63af71bc3ae6f9ebf9fcf8bd542c0e02d40b`:
+
+- [serial route](https://github.com/zts212653/clowder-ai/blob/028a63af71bc3ae6f9ebf9fcf8bd542c0e02d40b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts)
+- [parallel route](https://github.com/zts212653/clowder-ai/blob/028a63af71bc3ae6f9ebf9fcf8bd542c0e02d40b/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts)
+- [Signals lookup](https://github.com/zts212653/clowder-ai/blob/028a63af71bc3ae6f9ebf9fcf8bd542c0e02d40b/packages/api/src/domains/signals/services/signal-thread-lookup.ts)
+
+## Observations and limits
+
+In the fork after [Clowder PR1398](https://github.com/zts212653/clowder-ai/pull/1398)'s candidate was merged, one idle-input admission measured 1,056ms from ingress to execution acceptance: Queue preparation 14.27ms, route preparation 984.82ms, response admission 3.27ms. Within route preparation, memory/feedback took 467.75ms and identity/linked-context 354.85ms. The later 8.9s native Codex client startup is a separate problem, not an explanation for this internal second.
+
+A synthetic, isolated file fixture (no user data, Redis or model service) reproduced the Signals lookup cost for an unrelated thread: 500 articles took 27–28ms; 2,000 took 106–111ms, all with zero matches. This proves unnecessary work grows with the article library; it does **not** attribute the entire live 354.85ms bucket to Signals.
+
+## Audit scope
+
+The following preparation surfaces merit an inventory and cost/necessity check. Only the Signals scan above is confirmed unnecessary here; the other rows are investigation targets, not proven defects.
+
+| Surface | Current preparation work | Audit question |
+| --- | --- | --- |
+| Signals articles | Global inbox + sequential article metadata discovery; snippets/history added automatically | Use existing `signal_search` / `signal_get_article` on demand instead of scanning on ordinary sends |
+| Entity nudges | Alias discovery against evidence data | Is work bounded to the current input and necessary before acceptance? |
+| Proactive memory | Owner-scoped message-window scan, privacy checks, candidate registry lookup | Avoid repeatedly hydrating a broad window for each send; preserve owner/privacy boundaries |
+| Disposition feedback | Up to 50 lexical candidates, sequential exact-subject resolution and ledger reads | Repeated discovery/query cost versus indexed exact subjects; preserve truth checks |
+| Proposal status | Up to 200 recent messages scanned for proposal cards, then live candidate checks | Explicit proposal references/status questions versus unconditional discovery |
+| Concierge search | Automatically composed search context where enabled | What user/task evidence makes retrieval needed? |
+| Always-on evidence / world / packs | Conditional document, world or pack context | Explicit activation, bounded reads and prompt budget |
+| Identity / routing / session / message history | Core execution and continuity context | Keep correctness; measure separately from optional capabilities |
+
+## Proposed outcome
+
+1. Ordinary sends do not search Signals automatically. Article tools, study/link actions and stored content remain available.
+2. Classify each preparation fetch as execution-required, explicitly activated, or on-demand; document the trigger and real consumption path.
+3. Measure internal admission independently of provider startup, with per-operation timings and large-library/no-match fixtures.
+4. Remove unnecessary work at its ownership boundary rather than adding per-source conditions, an unconditional cache, or moving slow work behind an earlier status label.
+5. Preserve authorization, privacy, durable lifecycle and session recovery behavior.
+
+A local fork follow-up currently removes the automatic Signals dependency from both routing strategies and has red→green serial/parallel regressions. It is not yet reviewed/merged; no claim that all admission latency is solved. Remaining memory/feedback cost is still under investigation.
+
+Related: [Clowder issue839](https://github.com/zts212653/clowder-ai/issues/839) inventories injection visibility; this issue focuses on **retrieval necessity and latency on ordinary sends**, not another Console editor or runtime injection framework.
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/internal-preparation-timings.json b/feature-specs/evidence/f117-feedback-2026-10-09/internal-preparation-timings.json
new file mode 100644
index 0000000000..ed02bf6e74
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/internal-preparation-timings.json
@@ -0,0 +1,106 @@
+{
+ "runtimeHead": "d015c806d91839658788a32593735e3f4ff99254",
+ "runtimePid": 75466,
+ "contentIncluded": false,
+ "summary": {
+ "memory_and_feedback": {
+ "count": 36,
+ "minMs": 0.04179200017824769,
+ "medianMs": 0.30466649995651096,
+ "maxMs": 527.0105419999454
+ },
+ "identity_and_linked_context": {
+ "count": 36,
+ "minMs": 203.08762499992736,
+ "medianMs": 310.81531199999154,
+ "maxMs": 1403.3772499999031
+ }
+ },
+ "admissions": [
+ {
+ "time": "2026-10-09T12:26:12.091Z",
+ "threadId": "thread_msr51149hym0i79f",
+ "entryId": "queue:b740c70f814b0848e485069e9d67dc27",
+ "invocationId": "299f34ba-59c2-4dc0-87ec-4b00d853fd33",
+ "queueAgeMs": 1056,
+ "queuePreparationMs": 14.272583000012673,
+ "routePreparationMs": 984.8188340000343,
+ "responseAdmissionMs": 3.2681249999441206
+ }
+ ],
+ "runs": [
+ {
+ "threadId": "thread_msr51149hym0i79f",
+ "parentInvocationId": "c37d60ee-f4c0-4b35-9e61-c9bce570eac8",
+ "stages": [
+ {
+ "time": "2026-10-09T12:26:11.143Z",
+ "stage": "lineage_and_participants",
+ "durationMs": 5.342083999887109,
+ "elapsedMs": 5.342083999887109
+ },
+ {
+ "time": "2026-10-09T12:26:11.149Z",
+ "stage": "thread_and_workflow",
+ "durationMs": 6.126666000112891,
+ "elapsedMs": 11.46875
+ },
+ {
+ "time": "2026-10-09T12:26:11.149Z",
+ "stage": "guide_and_concierge",
+ "durationMs": 0.147374999942258,
+ "elapsedMs": 11.616124999942258
+ },
+ {
+ "time": "2026-10-09T12:26:11.149Z",
+ "stage": "concierge_search",
+ "durationMs": 0.009959000046364963,
+ "elapsedMs": 11.626083999988623
+ },
+ {
+ "time": "2026-10-09T12:26:11.617Z",
+ "stage": "memory_and_feedback",
+ "durationMs": 467.75312499993015,
+ "elapsedMs": 479.3792089999188
+ },
+ {
+ "time": "2026-10-09T12:26:11.640Z",
+ "stage": "routing_preflight",
+ "durationMs": 22.709625000017695,
+ "elapsedMs": 502.08883399993647
+ },
+ {
+ "time": "2026-10-09T12:26:11.645Z",
+ "stage": "source_and_pack",
+ "durationMs": 5.108291000011377,
+ "elapsedMs": 507.19712499994785
+ },
+ {
+ "time": "2026-10-09T12:26:11.646Z",
+ "stage": "service_capacity_and_session",
+ "durationMs": 1.44074999995064,
+ "elapsedMs": 508.6378749998985
+ },
+ {
+ "time": "2026-10-09T12:26:12.001Z",
+ "stage": "identity_and_linked_context",
+ "durationMs": 354.8482920000097,
+ "elapsedMs": 863.4861669999082
+ },
+ {
+ "time": "2026-10-09T12:26:12.070Z",
+ "stage": "history_and_prompt",
+ "durationMs": 69.11025000002701,
+ "elapsedMs": 932.5964169999352
+ },
+ {
+ "time": "2026-10-09T12:26:12.070Z",
+ "stage": "callback_setup",
+ "durationMs": 0.10162500001024455,
+ "elapsedMs": 932.6980419999454
+ }
+ ]
+ }
+ ],
+ "scope": "Anonymous aggregate of 36 runtime preparations; detailed coordinates restricted to this feedback thread."
+}
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-api.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-api.log.gz
new file mode 100644
index 0000000000..a2c0b6e056
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-api.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-web.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-web.log.gz
new file mode 100644
index 0000000000..145b9e25e4
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-web.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-preview-check.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-preview-check.log.gz
new file mode 100644
index 0000000000..cd4e2f6b6d
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-preview-check.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-row-interaction-red.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-row-interaction-red.log.gz
new file mode 100644
index 0000000000..80f62b02d8
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-row-interaction-red.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sender-display-red.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sender-display-red.log.gz
new file mode 100644
index 0000000000..8f79019424
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sender-display-red.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-api-tsc.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-api-tsc.log.gz
new file mode 100644
index 0000000000..99015495eb
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-api-tsc.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-autoload-red.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-autoload-red.log.gz
new file mode 100644
index 0000000000..04391ce62d
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-autoload-red.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-single-status-red.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-single-status-red.log.gz
new file mode 100644
index 0000000000..987422ebb5
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-single-status-red.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-source-guidance-original-red.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-source-guidance-original-red.log.gz
new file mode 100644
index 0000000000..364ec4bb8c
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-source-guidance-original-red.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sqlite-install.log.gz b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sqlite-install.log.gz
new file mode 100644
index 0000000000..73be4c7c6c
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sqlite-install.log.gz differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/pr-audit.json b/feature-specs/evidence/f117-feedback-2026-10-09/pr-audit.json
new file mode 100644
index 0000000000..88f3285dad
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/pr-audit.json
@@ -0,0 +1,65 @@
+{
+ "closedAndProviderVerified": [191, 199, 211, 212, 213, 214, 216, 210],
+ "doNotMerge": {
+ "185": {
+ "additions": 61419,
+ "baseRefName": "develop_base",
+ "baseRefOid": "3e70e1d6805be24672e8f841861f180d20b184c2",
+ "deletions": 128976,
+ "headRefName": "fix/f192-evidence-gate-durable",
+ "headRefOid": "bf13816d740d61207871a1a82e570de98d8742bd",
+ "isDraft": false,
+ "number": 185,
+ "state": "OPEN",
+ "title": "fix(F192): restore evidence gate + guard fork-only patches against rebuild loss"
+ },
+ "210": {
+ "additions": 82242,
+ "baseRefName": "develop_base",
+ "baseRefOid": "3e70e1d6805be24672e8f841861f180d20b184c2",
+ "deletions": 164536,
+ "headRefName": "fix/f117-quote-context",
+ "headRefOid": "0c24872112df67856091379a9061674080cf09f4",
+ "isDraft": true,
+ "number": 210,
+ "state": "CLOSED",
+ "title": "fix(F117): \u8865\u9f50\u5f15\u7528\u8bc4\u8bba\u8bfb\u53d6\u6295\u5f71\u4e0e\u6392\u961f\u94bb\u53d6\u8fb9\u754c",
+ "closedAt": "2026-10-09T13:58:25Z"
+ },
+ "215": {
+ "additions": 83110,
+ "baseRefName": "develop_base",
+ "baseRefOid": "3e70e1d6805be24672e8f841861f180d20b184c2",
+ "deletions": 164599,
+ "headRefName": "fix/fork-verdict-publish-classification",
+ "headRefOid": "96384e3ef10ae63dd18cab6354af1a0edc9ee13d",
+ "isDraft": true,
+ "number": 215,
+ "state": "OPEN",
+ "title": "fix: bind fork sync guard to the actual PR candidate"
+ }
+ },
+ "originalSlices": {
+ "185": {
+ "range": "aa409203b..bf13816d74",
+ "files": 11,
+ "additions": 1787,
+ "deletions": 1,
+ "purpose": "Missing-evidence daily eval gate plus fork rebuild guard/registry/workflow"
+ },
+ "210": {
+ "range": "9f235f083e..0c24872112",
+ "files": 5,
+ "additions": 427,
+ "deletions": 23,
+ "purpose": "Preserve quoted comment/contentBlocks in MCP reads, previews, keyword ranking and bounded drilldown"
+ },
+ "215": {
+ "range": "d35f3b8e54..96384e3ef1",
+ "files": 4,
+ "additions": 838,
+ "deletions": 79,
+ "purpose": "Published-main sync classification, remote candidate/base and CLI argv binding guard"
+ }
+ }
+}
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview-delivered.png b/feature-specs/evidence/f117-feedback-2026-10-09/preview-delivered.png
new file mode 100644
index 0000000000..846207a065
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/preview-delivered.png differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview-mobile.png b/feature-specs/evidence/f117-feedback-2026-10-09/preview-mobile.png
new file mode 100644
index 0000000000..88f45aa379
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/preview-mobile.png differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview-read.png b/feature-specs/evidence/f117-feedback-2026-10-09/preview-read.png
new file mode 100644
index 0000000000..b7f988ae6b
Binary files /dev/null and b/feature-specs/evidence/f117-feedback-2026-10-09/preview-read.png differ
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview-result.json b/feature-specs/evidence/f117-feedback-2026-10-09/preview-result.json
new file mode 100644
index 0000000000..390a75e40f
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/preview-result.json
@@ -0,0 +1,24 @@
+{
+ "kind": "current-components-with-in-memory-API",
+ "url": "http://127.0.0.1:49173/",
+ "runtimeDataUsed": false,
+ "providerUsed": false,
+ "duplicateBanner": false,
+ "themeDot": "rgb(167, 139, 250)",
+ "exactDeliveryAndReadTooltips": true,
+ "messageRowToggle": true,
+ "keyboardToggle": true,
+ "tooltipDelayMs": 1000,
+ "cancelTargets": [
+ {
+ "path": "/api/threads/feedback-preview/executions/live/one/cancel",
+ "body": "{\"catId\":\"opus\"}"
+ },
+ {
+ "path": "/api/threads/feedback-preview/executions/live/two/cancel",
+ "body": "{\"catId\":\"codex\"}"
+ }
+ ],
+ "mobileStopWithinViewport": true,
+ "errors": []
+}
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview/check.mjs b/feature-specs/evidence/f117-feedback-2026-10-09/preview/check.mjs
new file mode 100644
index 0000000000..e853fc983c
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/preview/check.mjs
@@ -0,0 +1,80 @@
+import { chromium } from 'file:///Users/lang/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs';
+import assert from 'node:assert/strict';
+import { writeFile } from 'node:fs/promises';
+
+const browser = await chromium.launch({
+ headless: true,
+ executablePath:
+ '/Users/lang/Library/Caches/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-mac-arm64/chrome-headless-shell',
+});
+const page = await browser.newPage({ viewport: { width: 1100, height: 850 } });
+const errors = [];
+page.on('pageerror', (error) => errors.push(error.message));
+await page.route('**/*', (route) => {
+ const url = new URL(route.request().url());
+ return url.origin === 'http://127.0.0.1:49173' ? route.continue() : route.abort();
+});
+try {
+ await page.goto('http://127.0.0.1:49173/');
+ await page.getByPlaceholder('继续输入,消息可不中断追加给当前成员...').waitFor();
+ assert.equal(await page.getByTestId('active-invocation-banner').count(), 0);
+ const stop = page.getByRole('button', { name: 'Stop generation', exact: true });
+ assert.equal(await stop.count(), 1);
+ assert.equal(await stop.getAttribute('title'), '停止当前对话全部回复');
+ assert.notEqual(await stop.evaluate((element) => getComputedStyle(element).backgroundColor), 'rgba(0, 0, 0, 0)');
+ const delivered = page.getByRole('button', { name: /^lang · 投递于: 10\/09 12:26:02 · / });
+ await delivered.hover();
+ await page.getByRole('tooltip').filter({ hasText: '投递于: 10/09 12:26:02' }).waitFor();
+ assert.equal(await page.getByText('读取状态不可用', { exact: false }).count(), 0);
+ await page.screenshot({ path: '/tmp/f117-feedback-preview.png', fullPage: true });
+ await page.keyboard.press('Escape');
+ const read = page.getByRole('button', { name: /^lang · 读取于: 10\/09 12:26:03 · / });
+ await read.hover();
+ await page.getByRole('tooltip').filter({ hasText: '读取于: 10/09 12:26:03' }).waitFor();
+ await page.screenshot({ path: '/tmp/f117-feedback-preview-read.png', fullPage: true });
+ const color = await page
+ .locator('[data-append-delivery-state] span')
+ .first()
+ .evaluate((element) => getComputedStyle(element).backgroundColor);
+ assert.equal(color, 'rgb(167, 139, 250)');
+ assert.equal(await page.getByText('展开全文', { exact: true }).count(), 0);
+ await delivered.click();
+ assert.equal(await delivered.getAttribute('aria-expanded'), 'true');
+ await delivered.press('Enter');
+ assert.equal(await delivered.getAttribute('aria-expanded'), 'false');
+ await stop.click();
+ await page.waitForFunction(
+ () => window.fixtureRequests.filter((request) => request.path.includes('/cancel')).length === 2,
+ );
+ const cancels = await page.evaluate(() =>
+ window.fixtureRequests.filter((request) => request.path.includes('/cancel')),
+ );
+ assert.deepEqual(cancels.map((request) => JSON.parse(request.body).catId).sort(), ['codex', 'opus']);
+ assert.equal(await page.getByText('猫猫正在回复中', { exact: false }).count(), 0);
+ await page.setViewportSize({ width: 430, height: 800 });
+ await page.reload();
+ await page.getByPlaceholder('继续输入,消息可不中断追加给当前成员...').waitFor();
+ const bounds = await page.getByRole('button', { name: 'Stop generation', exact: true }).boundingBox();
+ assert(bounds && bounds.x >= 0 && bounds.x + bounds.width <= 430);
+ await page.screenshot({ path: '/tmp/f117-feedback-preview-mobile.png', fullPage: true });
+ assert.equal(errors.length, 0);
+ const result = {
+ kind: 'current-components-with-in-memory-API',
+ url: 'http://127.0.0.1:49173/',
+ runtimeDataUsed: false,
+ providerUsed: false,
+ duplicateBanner: false,
+ themeDot: color,
+ exactDeliveryAndReadTooltips: true,
+ messageRowToggle: true,
+ keyboardToggle: true,
+ tooltipDelayMs: 1000,
+ cancelTargets: cancels,
+ mobileStopWithinViewport: true,
+ errors,
+ };
+ await writeFile('/tmp/f117-feedback-preview-result.json', JSON.stringify(result, null, 2));
+ console.log(JSON.stringify(result));
+} finally {
+ await browser.close();
+}
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview/index.html b/feature-specs/evidence/f117-feedback-2026-10-09/preview/index.html
new file mode 100644
index 0000000000..5e4fc93854
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/preview/index.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview/main.tsx b/feature-specs/evidence/f117-feedback-2026-10-09/preview/main.tsx
new file mode 100644
index 0000000000..3fa91b5b20
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/preview/main.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { AppendedInputReceipts } from '@/components/AppendedInputReceipts';
+import { ChatInput } from '@/components/ChatInput';
+import { ReplyPill } from '@/components/ReplyPill';
+import { ThreadExecutionBar } from '@/components/ThreadExecutionBar';
+import { useActiveExecutionStore } from '@/stores/activeExecutionStore';
+import { useChatStore } from '@/stores/chatStore';
+import '@/app/theme-tokens.css';
+import '@/app/theme-extras.css';
+import '@/app/connector-tokens.css';
+import '@/app/console-tokens.css';
+import '@/app/console-shell.css';
+import '@/app/console-controls.css';
+import '@/app/shell-v2.css';
+import '@/app/cat-persona-tokens.css';
+import '@/app/cat-persona-derived.css';
+import '@/app/globals.css';
+
+const cat = {
+ id: 'opus',
+ displayName: '布偶猫',
+ variantLabel: 'opus',
+ color: { primary: '#A78BFA', secondary: '#EDE9FE' },
+ avatar: '',
+ mentionPatterns: ['@opus'],
+ clientId: 'claude',
+ carrier: 'cli',
+ defaultModel: 'test',
+ roleDescription: 'preview',
+ personality: '',
+ messageDeliveryCapabilities: { guideReply: true },
+};
+const threadId = 'feedback-preview';
+const now = new Date(2026, 9, 9, 12, 26, 0).getTime();
+const execution = (id) => ({
+ executionId: id,
+ threadId,
+ catId: id === 'one' ? 'opus' : 'codex',
+ kind: 'live_invocation',
+ startedAt: Date.now() - 5000,
+ cancelability: {
+ state: 'cancelable',
+ target: { kind: 'live_invocation', threadId, catId: id === 'one' ? 'opus' : 'codex', executionId: id },
+ },
+});
+window.fixtureExecutions = [execution('one'), execution('two')];
+window.fixtureRequests = [];
+window.fixtureCats = [
+ cat,
+ {
+ ...cat,
+ id: 'codex',
+ displayName: '缅因猫',
+ variantLabel: 'sol',
+ color: { primary: '#6B9B68', secondary: '#DDEADC' },
+ mentionPatterns: ['@sol'],
+ },
+];
+useChatStore.setState({
+ currentThreadId: threadId,
+ threads: [
+ {
+ id: threadId,
+ title: 'preview',
+ projectPath: '/fixture',
+ createdAt: now,
+ lastActiveAt: now,
+ participants: [],
+ createdBy: 'preview',
+ },
+ ],
+ targetCats: ['opus'],
+ hasActiveInvocation: true,
+ activeInvocations: { one: { catId: 'opus', mode: 'execute' }, two: { catId: 'codex', mode: 'execute' } },
+});
+const store = useActiveExecutionStore.getState();
+const version = store.beginHydration(threadId, '/fixture');
+store.applySnapshot(threadId, version, { projectPath: '/fixture', executions: window.fixtureExecutions });
+const response = {
+ id: 'response',
+ type: 'assistant',
+ from: { kind: 'agent', catId: 'opus' },
+ catId: 'opus',
+ content: '收到',
+ timestamp: now,
+ lifecycle: {
+ kind: 'response',
+ orderKey: 'r',
+ invocationId: 'invocation',
+ targetId: 'opus',
+ inputEntryIds: ['initial', 'a', 'b'],
+ inputMessageIds: ['initial', 'a', 'b'],
+ status: 'processing',
+ startedAt: now,
+ },
+};
+const source = (id, read) => ({
+ id,
+ type: 'user',
+ from: { kind: 'user', userId: 'preview' },
+ content: read ? '这条有精确读取回执' : '这条只确认已投递。'.repeat(35),
+ timestamp: now + 1000,
+ lifecycle: {
+ kind: 'input',
+ orderKey: id,
+ entryId: id,
+ dispatchRefs: [
+ {
+ targetId: 'opus',
+ statusMessageId: 'response',
+ dispatchedAt: now + 2000,
+ status: 'dispatched',
+ ...(read ? { inputRead: { status: 'read', at: now + 3000 } } : {}),
+ },
+ ],
+ },
+});
+createRoot(document.getElementById('root')).render(
+
+ F117 当前组件隔离预览
+ 使用合成输入与内存 API;不连接运行实例或模型。
+ window.fixtureCats.find((c) => c.id === id)}
+ />
+ window.fixtureCats.find((c) => c.id === id)}
+ />
+
+
+ true} />
+
+ ,
+);
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/preview/server.mjs b/feature-specs/evidence/f117-feedback-2026-10-09/preview/server.mjs
new file mode 100644
index 0000000000..8122c7bc8e
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/preview/server.mjs
@@ -0,0 +1,56 @@
+import { createRequire } from 'node:module';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import autoprefixer from 'autoprefixer';
+import tailwindcss from 'tailwindcss';
+import { createServer } from 'vite';
+
+const root = path.dirname(fileURLToPath(import.meta.url));
+const web = path.resolve(root, '../../../../packages/web');
+const api = `export const API_URL=''; export async function apiFetch(path,init={}) { window.fixtureRequests.push({path,body:init.body}); let body={}; if(path==='/api/cats')body={cats:window.fixtureCats}; else if(path==='/api/config')body={config:{coCreator:{name:'lang',aliases:[],mentionPatterns:['@lang']}}}; else if(path.includes('message-disposition'))body={productDefault:'next_work',global:'continue_current',thread:null,effective:'continue_current',source:'global'}; else if(path.includes('/executions/live/') && init.method==='POST'){const id=decodeURIComponent(path.split('/executions/live/')[1].split('/')[0]);window.fixtureExecutions=window.fixtureExecutions.filter(e=>e.executionId!==id);} else if(path.includes('/api/executions/active'))body={projectPath:'/fixture',executions:window.fixtureExecutions}; return new Response(JSON.stringify(body),{status:200,headers:{'content-type':'application/json'}}); }`;
+const server = await createServer({
+ root,
+ configFile: false,
+ server: {
+ host: '127.0.0.1',
+ port: Number(process.argv[2]),
+ strictPort: true,
+ fs: { allow: [path.resolve(web, '../..')] },
+ },
+ resolve: {
+ alias: [
+ { find: '@/utils/api-client', replacement: 'virtual:fixture-api' },
+ { find: 'next/navigation', replacement: 'virtual:fixture-nav' },
+ { find: '@', replacement: path.join(web, 'src') },
+ { find: '@ricky0123/vad-web', replacement: path.join(web, 'src/__mocks__/vad-web.ts') },
+ ],
+ },
+ plugins: [
+ {
+ name: 'in-memory-preview-api',
+ resolveId(id) {
+ if (id.startsWith('virtual:fixture-')) return '\0' + id;
+ },
+ load(id) {
+ if (id === '\0virtual:fixture-api') return api;
+ if (id === '\0virtual:fixture-nav')
+ return 'export const useRouter=()=>({push(){}}); export const usePathname=()=>"/fixture"; export const useSearchParams=()=>new URLSearchParams();';
+ },
+ },
+ ],
+ define: { 'process.env': '{}' },
+ esbuild: { jsx: 'automatic' },
+ css: {
+ postcss: {
+ plugins: [
+ tailwindcss({
+ ...createRequire(import.meta.url)(path.join(web, 'tailwind.config.js')),
+ content: [path.join(web, 'src/**/*.{js,ts,jsx,tsx,mdx}')],
+ }),
+ autoprefixer(),
+ ],
+ },
+ },
+});
+await server.listen();
+console.log(server.resolvedUrls.local[0]);
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/probe-internal-preparation.mjs b/feature-specs/evidence/f117-feedback-2026-10-09/probe-internal-preparation.mjs
new file mode 100644
index 0000000000..54a8f2af1c
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/probe-internal-preparation.mjs
@@ -0,0 +1,57 @@
+// Diagnostic reproduction only: synthetic file data, no runtime Redis, no
+// model/client, no production data, no changes to the process being diagnosed.
+import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { createSignalArticleLookup } from '../../../packages/api/dist/domains/signals/services/signal-thread-lookup.js';
+
+const ownedRoot = await mkdtemp(join(tmpdir(), 'f117-preparation-probe-'));
+const results = [];
+for (const recordCount of [0, 500, 2_000]) {
+ const root = join(ownedRoot, String(recordCount));
+ const inboxDir = join(root, 'inbox');
+ const libraryDir = join(root, 'library');
+ await mkdir(inboxDir, { recursive: true });
+ await mkdir(libraryDir, { recursive: true });
+ const records = [];
+ for (let index = 0; index < recordCount; index++) {
+ const id = `synthetic-${index}`;
+ const articleDir = join(libraryDir, id);
+ await mkdir(articleDir);
+ await writeFile(
+ join(articleDir, 'meta.json'),
+ JSON.stringify({
+ articleId: id,
+ threads: [],
+ artifacts: [],
+ collections: [],
+ }),
+ );
+ records.push({
+ id,
+ title: id,
+ url: 'https://example.invalid',
+ source: 'synthetic',
+ tier: 1,
+ fetchedAt: '2026-10-09T00:00:00Z',
+ filePath: `${articleDir}.md`,
+ });
+ }
+ await writeFile(join(inboxDir, '2026-10-09.json'), JSON.stringify(records));
+ process.env.SIGNALS_ROOT_DIR = root; // This diagnostic child only.
+ const lookup = createSignalArticleLookup();
+ const samples = [];
+ for (let attempt = 0; attempt < 3; attempt++) {
+ const started = performance.now();
+ const found = await lookup('unlinked-synthetic-thread');
+ samples.push({ durationMs: performance.now() - started, matches: found.length });
+ }
+ results.push({ recordCount, samples });
+}
+console.log(
+ JSON.stringify(
+ { kind: 'synthetic-unlinked-signal-lookup', ownedRoot, runtimeDataUsed: false, providerUsed: false, results },
+ null,
+ 2,
+ ),
+);
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/sha256.json b/feature-specs/evidence/f117-feedback-2026-10-09/sha256.json
new file mode 100644
index 0000000000..6ff0baabcb
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/sha256.json
@@ -0,0 +1,56 @@
+{
+ "feature-specs/evidence/f117-feedback-2026-10-09/README.md": "16f1879e24b07c25f511fc03c63170dcee724e33e0d499a15b2cb75a9c591c97",
+ "feature-specs/evidence/f117-feedback-2026-10-09/check-web-types.cjs": "eb8e4524bd69d36bec50c60472f8b88a934b0ad6eafff545ec9ecf4e6b89a1e8",
+ "feature-specs/evidence/f117-feedback-2026-10-09/context-audit-issue.md": "e91f2828b7089946a4b70315128967362a17a8d20d5e891e121b150f5ac67035",
+ "feature-specs/evidence/f117-feedback-2026-10-09/internal-preparation-timings.json": "7b16232a75557328f28980d54779ab9da05648803d49de946faa47f2cea748ee",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-api.log.gz": "36a2fb476fe29bda24913c25019944e4caf7f1b86b6d5d989d876e7945a4be10",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-final-web.log.gz": "40201459a568c35b85434fbb5d78fa478dd8633c89477c1aba0b5f247dd6acf1",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-feedback-preview-check.log.gz": "62b5a80c8ff08a28371b89e4f0af659b505c75d07f225b943df58c86d9e81920",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-row-interaction-red.log.gz": "0ccf7c9aa7636ca4b58c314c2217dcf807bd9464d0aba93b5e2920cd8e461e57",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sender-display-red.log.gz": "6c18efb99bdaf39d03021ef5919480d72731d26ddb067e166c8f7b2d109c9e14",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-api-tsc.log.gz": "73ad7b48badb5bfb326e35ad32957a8ed64f2d1688af36e91fd6bb01e903c5c4",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-signal-autoload-red.log.gz": "b64893effbd784764b1ab9da024c6ad04ebbdc81083c073fd3b53fe1ee208af9",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-single-status-red.log.gz": "12cf010126c638c2d8bfb2243809a874393cbef4749b5a5e135e7e3f7b8f4ee4",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-source-guidance-original-red.log.gz": "84202d65fd36bbecec4783b8ede662d38d33d144d3067cb5ec647b52f6cfd7cc",
+ "feature-specs/evidence/f117-feedback-2026-10-09/logs/f117-sqlite-install.log.gz": "ce224337bc85f288bdd134da5c3137cd4a161f12d895685b75de474548070cb6",
+ "feature-specs/evidence/f117-feedback-2026-10-09/pr-audit.json": "c27e4ca8685097deb4da8fda357470564684090adda59370dee7f6fcdf7715b2",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview-delivered.png": "26f62f40a60a487ef486c0c537b807402fdd59fc52238f2dba3d39c9ca54c181",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview-mobile.png": "fc5c5ca540406ea9f398031d87d149296788ed6581c93df4057d64889907ad38",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview-read.png": "c97852bb1dad181f9d94737fd78357020d78e27f537ee4161e30e32cc282d7fa",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview-result.json": "aff074ccf2b4df14605e825d3fa31cc138de63e8cc08dacd754e60e2a1e50fcb",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview/check.mjs": "bb2c20b5c1f486056f7107bab1c958405d321ce37b6925a1b48bd8800bb20218",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview/index.html": "8d1cf5289353d45541bd4084ef0b983a2932a067aa47724ddf8bcf32ed133aa5",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview/main.tsx": "c2f8565409b58f897870e53b431e6587d7ca01035b3e4adfb716b2025601b060",
+ "feature-specs/evidence/f117-feedback-2026-10-09/preview/server.mjs": "d4f25764d37f1d84114e987aae5f528e2fd1096850c360eb3ad843420f15b0fb",
+ "feature-specs/evidence/f117-feedback-2026-10-09/probe-internal-preparation.mjs": "b0aefc9c418a18d368e5384bf5973958521879de40c4e9b6755107950d050dda",
+ "feature-specs/evidence/f117-feedback-2026-10-09/signal-library-probe.json": "25a07d3e7856fe54f366ed4ec1fe12b169130944e45cbceabf2989cc890e3361",
+ "feature-specs/evidence/f117-feedback-2026-10-09/web-type-comparison.json": "99e3cedad9869fd9f2cf3d58e12b2ddaea7cac104e711ded7e1364b143656e04",
+ "packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts": "bbf2686a5aefc7cb2ccdb7964ada94eee2cf7833c39f651fbc61ed09f8d40a29",
+ "packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts": "2aa3c33534955f61b08e33d29cccd9e7bc17ace7d3af5c38d0cb18e2bc1ce7c3",
+ "packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts": "621a6aca8673b7e99bc1bb125156d88a2e8751cd29db72f4fc69bc35bdf7878b",
+ "packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts": "ef138a0170451e0dad85928cb210989595028e6bba28bb55e1ffe4b04e0a86a8",
+ "packages/api/src/domains/cats/services/agents/routing/route-parallel.ts": "e6c8ee37ae2b4d5db4e240286edcf77b583ef4d7f0a45fac64d730b43b48b083",
+ "packages/api/src/domains/cats/services/agents/routing/route-serial.ts": "1b092abd0960bf159e12573e3140b4b4f90e0ff48b46db7be8967a9e436cf04c",
+ "packages/api/src/index.ts": "2fec79e64cc95f40a742b147cb3c080a35fb56a2fcfc9f9c04e71bf231e578e7",
+ "packages/api/src/routes/queue.ts": "e8526fd1524572594862501d8770819e56e6750f3756f79634dcd890be0c8b7d",
+ "packages/api/test/1398-append-delivery-boundary.test.js": "c9d9cb27031cf083c7100a4a9275bce5650093dcda95a9f2a505e0a73233631f",
+ "packages/api/test/agent-router.test.js": "895cbd4fbf2426a051c6fb64870fcb69c90f9de84c51738433b9a9bb0d02b785",
+ "packages/api/test/queue-api.test.js": "b8a69b426f9441f4db40c1e5e3f8a3bfed2d30ea037df77fe366d4ab0b69d6d8",
+ "packages/web/src/components/AppTooltip.tsx": "ae796d1cec7dbd7537edbeca2506d1fb90a2db9bf54c5dd9bc3a883aa1f142f1",
+ "packages/web/src/components/AppendedInputReceipts.tsx": "28dbea7c71b8594d113abc72551a0a9618a8a3c13dcee096ed815c2f9d8eb570",
+ "packages/web/src/components/ChatInput.tsx": "3960bef3af78f19c4b9603f3b6a599e5d8f10246e194f9b01f5317819ca6ac2c",
+ "packages/web/src/components/ChatInputActionButton.tsx": "2348fa3c5a3f6bf5cb13b4f519784afec8c8219c34d1284b725d5c52523796d8",
+ "packages/web/src/components/ReplyPill.tsx": "8d7495a3e9672d3834fc370a934ae688d6d9d553ba35b00f71148fe9394f4ac3",
+ "packages/web/src/components/__tests__/1398-sender-identity.test.tsx": "24e4bfeb7da3176c747ee347a4445e532e21793ae0b398df943ea20624364836",
+ "packages/web/src/components/__tests__/ChatMessage-nameplate.test.tsx": "b281115bc98b2fdea25185ecfef8a576649c0901f368f244a766bb6becc44843",
+ "packages/web/src/components/__tests__/ChatMessage-render-isolation.test.tsx": "5a88dd4dbc530f00bf97a3d318947d5251bcb9b335e27c69d0a1bfa935609762",
+ "packages/web/src/components/__tests__/ReplyPill.test.tsx": "b15975f1d381caffd0aae1444f6e974235f31093e81c8fa49a3278caebcb5b69",
+ "packages/web/src/components/__tests__/appended-input-receipts-render.test.tsx": "765f92fcb7ad970317d68585a5f3ba5659ee2ded8739ffb8ea9792ed6abfec87",
+ "packages/web/src/components/__tests__/chat-input-message-disposition.test.ts": "52353dff19296af5e77df7adec00334bdcaa63ae388a08f3efc8e7cff1130bff",
+ "packages/web/src/components/__tests__/chat-input-mobile.test.ts": "75343a49de8004582c1693ba1b33924391c57dbbe7a7d318f92b772915f3f78a",
+ "packages/web/src/components/__tests__/chat-input-v2-stop.test.tsx": "fd1e3bc061eef74ed99fab554e136a4b88b6fcc0776840c85bf2cb1a5b827b94",
+ "packages/web/src/components/__tests__/queue-panel-steer.test.ts": "a6380f06ad1f1ba31463a67d29048cc5c096fc573850271e60a2fa6d5aadbb05",
+ "packages/web/src/components/useQueueActionConvergence.ts": "f7469daf8765a3851f2df9a22ce6d6df7ef90821d42c111435880c1590ab0af3",
+ "packages/web/src/lib/__tests__/resolve-sender.test.ts": "655f75fb6c45e91b859206119d2628a3f401795eae1f4423d3679a3c955d3eb2",
+ "packages/web/src/lib/resolve-sender.ts": "8052a530c3440ddd889854fef290f977293d0d72ef3c5f208330c37ae8fefe6b"
+}
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/signal-library-probe.json b/feature-specs/evidence/f117-feedback-2026-10-09/signal-library-probe.json
new file mode 100644
index 0000000000..ef014d1c56
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/signal-library-probe.json
@@ -0,0 +1,59 @@
+{
+ "kind": "synthetic-unlinked-signal-lookup",
+ "ownedRoot": "/var/folders/1z/8dpvll5d2fq7brlf2qk4kkw40000gn/T/f117-preparation-probe-qDnoVA",
+ "runtimeDataUsed": false,
+ "providerUsed": false,
+ "results": [
+ {
+ "recordCount": 0,
+ "samples": [
+ {
+ "durationMs": 0.4635409999999922,
+ "matches": 0
+ },
+ {
+ "durationMs": 0.09633300000001555,
+ "matches": 0
+ },
+ {
+ "durationMs": 0.08283300000002214,
+ "matches": 0
+ }
+ ]
+ },
+ {
+ "recordCount": 500,
+ "samples": [
+ {
+ "durationMs": 26.996124999999978,
+ "matches": 0
+ },
+ {
+ "durationMs": 27.21391699999998,
+ "matches": 0
+ },
+ {
+ "durationMs": 27.87041700000003,
+ "matches": 0
+ }
+ ]
+ },
+ {
+ "recordCount": 2000,
+ "samples": [
+ {
+ "durationMs": 110.70041700000002,
+ "matches": 0
+ },
+ {
+ "durationMs": 106.08837500000004,
+ "matches": 0
+ },
+ {
+ "durationMs": 108.72775000000001,
+ "matches": 0
+ }
+ ]
+ }
+ ]
+}
diff --git a/feature-specs/evidence/f117-feedback-2026-10-09/web-type-comparison.json b/feature-specs/evidence/f117-feedback-2026-10-09/web-type-comparison.json
new file mode 100644
index 0000000000..1048582e01
--- /dev/null
+++ b/feature-specs/evidence/f117-feedback-2026-10-09/web-type-comparison.json
@@ -0,0 +1,21 @@
+{
+ "base": "d015c806d91839658788a32593735e3f4ff99254",
+ "replacedBaseFiles": 28,
+ "candidate": [
+ {
+ "file": "packages/web/src/hooks/__tests__/proposal-card-realtime-socket.integration.test.tsx",
+ "line": 152,
+ "code": 2322,
+ "message": "Type 'boolean | null' is not assignable to type 'boolean'.\n Type 'null' is not assignable to type 'boolean'."
+ }
+ ],
+ "baseDiagnostics": [
+ {
+ "file": "packages/web/src/hooks/__tests__/proposal-card-realtime-socket.integration.test.tsx",
+ "line": 152,
+ "code": 2322,
+ "message": "Type 'boolean | null' is not assignable to type 'boolean'.\n Type 'null' is not assignable to type 'boolean'."
+ }
+ ],
+ "sameDiagnostics": true
+}
diff --git a/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts b/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts
index 040e1294ea..a9782fb250 100644
--- a/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts
+++ b/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts
@@ -935,8 +935,8 @@ export class InvocationQueue {
}
/**
- * Bind one public human message to the selected member and persist the
- * author's non-interrupting delivery intent. This is the Queue-panel
+ * Bind one public conversation input to the selected member and persist
+ * the operator's non-interrupting delivery choice. This is the Queue-panel
* counterpart of composer admission: the same row either appends to the
* exact current run or remains ordinary next work without canceling it.
*/
@@ -949,13 +949,7 @@ export class InvocationQueue {
at = Date.now(),
): Promise {
const entry = this.findEntry(threadId, userId, entryId);
- if (
- !entry ||
- entry.status !== 'queued' ||
- entry.kind !== 'conversation_input' ||
- entry.from.kind !== 'user' ||
- isSystemPinnedQueueEntry(entry)
- ) {
+ if (!entry || entry.status !== 'queued' || entry.kind !== 'conversation_input' || isSystemPinnedQueueEntry(entry)) {
return null;
}
const assignsTargetlessConversation = entry.targets.length === 0;
diff --git a/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts b/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts
index e33aed9a97..b62fd21c52 100644
--- a/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts
+++ b/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts
@@ -838,9 +838,9 @@ export class QueueProcessor {
}
/**
- * Admission-owned automatic Queue -> Active Run transfer. Human work must
- * still be bound to the same parent selected by its continue-current intent;
- * agent A2A carriers have no human disposition and are eligible by default.
+ * Admission-owned automatic Queue -> Active Run transfer. An explicit delivery
+ * choice must remain bound to its selected parent for every source kind.
+ * Source identity does not determine the target's guidance capability.
* The exact run/capability/revision fences remain owned by the shared
* lifecycle projection and append transaction below.
*/
@@ -852,14 +852,14 @@ export class QueueProcessor {
}): Promise {
const { queue, invocationTracker } = this.deps;
const entry = queue.getEntrySnapshot(input.threadId, input.userId, input.entryId);
- if (!entry || entry.execution.liveSessionId || (entry.from.kind !== 'user' && entry.from.kind !== 'agent')) {
+ if (!entry || entry.execution.liveSessionId) {
return { outcome: 'rejected', reason: 'append_unavailable' };
}
- if (entry.from.kind === 'user') {
+ const requestedTargets = input.targetCatId ? [input.targetCatId] : queueEntryTargetCats(entry);
+ if (requestedTargets.some((targetId) => entry.delivery.authorIntentByTarget?.[targetId])) {
if (!invocationTracker.getExecutionId) {
return { outcome: 'rejected', reason: 'append_unavailable' };
}
- const requestedTargets = input.targetCatId ? [input.targetCatId] : queueEntryTargetCats(entry);
const remainsBoundToRequestedParent = requestedTargets.every((targetId) => {
const intent = entry.delivery.authorIntentByTarget?.[targetId];
return (
diff --git a/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts b/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts
index ecfe5fdcc1..a1faca3d1b 100644
--- a/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts
+++ b/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts
@@ -1171,18 +1171,6 @@ export interface InvocationDeps {
readonly guideSessionStore?: import('../../../../guides/GuideSessionRepository.js').IGuideSessionStore;
/** F155 B-6: Dismiss tracker for guide offer suppression */
readonly dismissTracker?: import('../../../../guides/GuideDismissTracker.js').IGuideDismissTracker;
- /** F091: Lookup signal articles linked to a thread for context injection */
- readonly signalArticleLookup?: (threadId: string) => Promise<
- readonly {
- id: string;
- title: string;
- source: string;
- tier: number;
- contentSnippet: string;
- note?: string | undefined;
- relatedDiscussions?: readonly { sessionId: string; snippet: string; score: number }[] | undefined;
- }[]
- >;
/** F229: Concierge config store for duty-cat岗位 prompt injection (optional, fail-open) */
readonly conciergeConfigStore?: import('../../../../concierge/ConciergeConfigStore.js').IConciergeConfigStore;
/** F229 Phase B: TriagePlan store for triage-plan marker → confirm/cancel card actions (optional, fail-open) */
diff --git a/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts b/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts
index 94897ac891..6c7fe7f98d 100644
--- a/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts
+++ b/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts
@@ -545,18 +545,6 @@ export interface AgentRouterOptions {
tmuxGateway?: import('../../../../terminal/tmux-gateway.js').TmuxGateway;
/** F089 Phase 2: agent pane registry for observability */
agentPaneRegistry?: import('../../../../terminal/agent-pane-registry.js').AgentPaneRegistry;
- /** F091: Signal article lookup for thread context injection */
- signalArticleLookup?: (threadId: string) => Promise<
- readonly {
- id: string;
- title: string;
- source: string;
- tier: number;
- contentSnippet: string;
- note?: string | undefined;
- relatedDiscussions?: readonly { sessionId: string; snippet: string; score: number }[] | undefined;
- }[]
- >;
/** F129: Pack store for loading active packs at invocation time */
packStore?: import('../../../../packs/PackStore.js').PackStore;
/** F148: Evidence store for hierarchical context recall */
@@ -655,18 +643,6 @@ export class AgentRouter {
private socketManager: import('../../../../../infrastructure/websocket/SocketManager.js').SocketManager | undefined;
private tmuxGateway: import('../../../../terminal/tmux-gateway.js').TmuxGateway | undefined;
private agentPaneRegistry: import('../../../../terminal/agent-pane-registry.js').AgentPaneRegistry | undefined;
- private signalArticleLookup?:
- | ((threadId: string) => Promise<
- readonly {
- id: string;
- title: string;
- source: string;
- tier: number;
- contentSnippet: string;
- note?: string | undefined;
- }[]
- >)
- | undefined;
private packStore?: import('../../../../packs/PackStore.js').PackStore;
private evidenceStore?: import('../../../../memory/interfaces.js').IEvidenceStore;
private proactiveMemoryNudgeService?: import('../../../../memory/ProactiveMemoryNudgeService.js').ProactiveMemoryNudgeService;
@@ -822,7 +798,6 @@ export class AgentRouter {
this.socketManager = options.socketManager;
this.tmuxGateway = options.tmuxGateway;
this.agentPaneRegistry = options.agentPaneRegistry;
- this.signalArticleLookup = options.signalArticleLookup;
this.packStore = options.packStore;
this.evidenceStore = options.evidenceStore;
this.proactiveMemoryNudgeService = options.proactiveMemoryNudgeService;
@@ -1554,7 +1529,6 @@ export class AgentRouter {
...(this.executionDigestStore ? { executionDigestStore: this.executionDigestStore } : {}),
...(this.tmuxGateway ? { tmuxGateway: this.tmuxGateway } : {}),
...(this.agentPaneRegistry ? { agentPaneRegistry: this.agentPaneRegistry } : {}),
- ...(this.signalArticleLookup ? { signalArticleLookup: this.signalArticleLookup } : {}),
...(this.guideSessionStore ? { guideSessionStore: this.guideSessionStore } : {}),
...(this.dismissTracker ? { dismissTracker: this.dismissTracker } : {}),
...(this.conciergeConfigStore ? { conciergeConfigStore: this.conciergeConfigStore } : {}),
diff --git a/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts b/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts
index dfc1b50be2..1ffba40c3c 100644
--- a/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts
+++ b/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts
@@ -570,26 +570,7 @@ export async function* routeParallel(
teammates: teammates.map((id) => id as string),
})
: '';
- // F091: Inject linked signal articles into context
- let activeSignals:
- | readonly {
- id: string;
- title: string;
- source: string;
- tier: number;
- contentSnippet: string;
- note?: string | undefined;
- relatedDiscussions?: readonly { sessionId: string; snippet: string; score: number }[] | undefined;
- }[]
- | undefined;
- if (deps.invocationDeps.signalArticleLookup) {
- try {
- const signals = await deps.invocationDeps.signalArticleLookup(threadId);
- if (signals.length > 0) activeSignals = signals;
- } catch {
- /* best-effort: signal lookup failure does not block invocation */
- }
- }
+ // Signal articles are retrieved through explicit signal tools, not ordinary delivery.
// F163 AC-A3: always_on constitutional docs injection (fail-open, flag-gated)
// shadow: query but do NOT inject into prompt (record-only for experiment diff)
@@ -630,7 +611,6 @@ export async function* routeParallel(
...(activeParticipants.length > 0 ? { activeParticipants } : {}),
...(routingPolicy ? { routingPolicy } : {}),
...(sopStageHint ? { sopStageHint } : {}),
- ...(activeSignals ? { activeSignals } : {}),
...(voiceMode ? { voiceMode } : {}),
...(bootcampState ? { bootcampState, threadId, bootcampMemberCount } : {}),
...(alwaysOnDocs && alwaysOnInjectionMode === 'on' ? { alwaysOnDocs } : {}),
diff --git a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts
index ac10b491ff..f30a8d31c5 100644
--- a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts
+++ b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts
@@ -744,6 +744,7 @@ export async function* routeSerial(
log.warn({ err: nudgeErr, threadId }, '[F260] entity nudge hook failed — fail-open, no nudges this invocation');
}
}
+ checkpointPreparation('entity_nudge');
if (deps.proactiveMemoryNudgeService && currentUserMessageId && options.frustrationAutoIssueEligible !== false) {
preparedProactiveMemoryNudge = await deps.proactiveMemoryNudgeService.prepare({
ownerUserId: userId,
@@ -751,6 +752,7 @@ export async function* routeSerial(
});
routeOwnedNudgePromptContext += preparedProactiveMemoryNudge.context;
}
+ checkpointPreparation('proactive_memory');
let humanDispositionFeedbackPromptContext = '';
if (
deps.humanDispositionFeedbackContextService &&
@@ -765,6 +767,7 @@ export async function* routeSerial(
log.warn({ err: feedbackErr, threadId }, '[F281] exact-subject feedback context failed closed for serial route');
}
}
+ checkpointPreparation('disposition_feedback');
if (deps.invocationDeps.memoryCuePromptService) {
memoryCueOpportunitySeeds.push(
...judgmentSurfaceCueSeeds({
@@ -782,7 +785,7 @@ export async function* routeSerial(
);
}
const routeLevelNudgePromptContext = routeOwnedNudgePromptContext + humanDispositionFeedbackPromptContext;
- checkpointPreparation('memory_and_feedback');
+ checkpointPreparation('memory_cues');
const completedCatInvocationIds: Array<[string, string]> = [];
const pushRecallPresentationsByInvocation = new Map();
@@ -1115,26 +1118,7 @@ export async function* routeSerial(
teammates: teammates.map((id) => id as string),
})
: '';
- // F091: Inject linked signal articles into context
- let activeSignals:
- | readonly {
- id: string;
- title: string;
- source: string;
- tier: number;
- contentSnippet: string;
- note?: string | undefined;
- relatedDiscussions?: readonly { sessionId: string; snippet: string; score: number }[] | undefined;
- }[]
- | undefined;
- if (deps.invocationDeps.signalArticleLookup) {
- try {
- const signals = await deps.invocationDeps.signalArticleLookup(threadId);
- if (signals.length > 0) activeSignals = signals;
- } catch {
- /* best-effort: signal lookup failure does not block invocation */
- }
- }
+ // Signal articles are retrieved through explicit signal tools, not ordinary delivery.
// F163 AC-A3: always_on constitutional docs injection (fail-open, flag-gated)
// shadow: query but do NOT inject into prompt (record-only for experiment diff)
@@ -1211,7 +1195,6 @@ export async function* routeSerial(
...(activeParticipants.length > 0 ? { activeParticipants } : {}),
...(routingPolicy ? { routingPolicy } : {}),
...(sopStageHint ? { sopStageHint } : {}),
- ...(activeSignals ? { activeSignals } : {}),
...(voiceMode ? { voiceMode } : {}),
...(bootcampState ? { bootcampState, bootcampMemberCount } : {}),
...(alwaysOnDocs && alwaysOnInjectionMode === 'on' ? { alwaysOnDocs } : {}),
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts
index 0869f7b70e..743d2a9406 100644
--- a/packages/api/src/index.ts
+++ b/packages/api/src/index.ts
@@ -268,7 +268,6 @@ import {
import { readRuntimeServiceReadiness } from './domains/runtime-deployment/RuntimeDeploymentServiceReadiness.js';
import { createRuntimeInteractionRuntime } from './domains/runtime-interaction/runtime-interaction-composition.js';
import { appendServiceLog } from './domains/services/service-lifecycle.js';
-import { createSignalArticleLookup } from './domains/signals/services/signal-thread-lookup.js';
import { FileTasteRepository } from './domains/taste/services/TasteRepository.js';
import { createVignetteWriter } from './domains/taste/services/writeVignette.js';
import { createTasteProposalStore } from './domains/taste/stores/factories/TasteProposalStoreFactory.js';
@@ -2734,7 +2733,6 @@ async function main(): Promise {
socketManager,
...(tmuxGateway ? { tmuxGateway } : {}),
...(agentPaneRegistry ? { agentPaneRegistry } : {}),
- signalArticleLookup: createSignalArticleLookup({ transcriptReader }),
packStore,
evidenceStore: memoryServices.evidenceStore,
...(proactiveMemoryNudgeService ? { proactiveMemoryNudgeService } : {}),
diff --git a/packages/api/src/routes/queue.ts b/packages/api/src/routes/queue.ts
index 42e4d7bf4c..104bb121ef 100644
--- a/packages/api/src/routes/queue.ts
+++ b/packages/api/src/routes/queue.ts
@@ -1459,7 +1459,7 @@ export const queueRoutes: FastifyPluginAsync = async (app, o
reply.status(409);
return { error: '条目正在处理中,无法改派', code: 'ENTRY_PROCESSING' };
}
- if (isSystemPinnedQueueEntry(entry) || entry.kind !== 'conversation_input' || entry.from.kind !== 'user') {
+ if (isSystemPinnedQueueEntry(entry) || entry.kind !== 'conversation_input') {
reply.status(409);
return { error: '该条目不支持不中断发送', code: 'ENTRY_CONTINUE_UNAVAILABLE' };
}
diff --git a/packages/api/test/1398-append-delivery-boundary.test.js b/packages/api/test/1398-append-delivery-boundary.test.js
index 97706e33ed..7522f997d6 100644
--- a/packages/api/test/1398-append-delivery-boundary.test.js
+++ b/packages/api/test/1398-append-delivery-boundary.test.js
@@ -40,7 +40,7 @@ function createHarness() {
return { ...deps, processor: new QueueProcessor(deps, { retryDeferral: { baseDelayMs: 60_000 } }) };
}
-async function admit(harness) {
+async function admit(harness, overrides = {}) {
sequence += 1;
const queueInput = canonicalTestQueueInput({
threadId: 'thread-1',
@@ -51,6 +51,7 @@ async function admit(harness) {
content: `append body ${sequence}`,
targetCats: ['opus'],
intent: 'execute',
+ ...overrides,
});
const result = await harness.queue.appendAndEnqueueDurable(
harness.messageStore,
@@ -63,6 +64,7 @@ async function admit(harness) {
mentions: ['opus'],
timestamp: Date.now(),
deliveryStatus: 'queued',
+ ...(overrides.messageSource ? { source: overrides.messageSource } : {}),
}),
queueInput,
);
@@ -136,6 +138,68 @@ const deliveredEmits = (harness) =>
const activeInputs = (harness) => harness.invocationTracker.getActiveSlots('thread-1')[0].activeRun.inputMessageIds;
describe('delivery owns Append admission, without model-read state', () => {
+ it('auto Append honors a connector target choice and exact parent, preserving source identity', async () => {
+ const harness = createHarness();
+ const from = { kind: 'external', connectorId: 'github-wait', sender: { id: 'github-wait' } };
+ const carrier = {
+ v: 1,
+ waitId: 'task-pr-216',
+ outcomeId: 'wait:pr:mindfn/clowder-ai:216:g1:matched',
+ ownerFence: { kind: 'containing_task', generation: 1 },
+ };
+ const admitted = await admit(harness, {
+ from,
+ waitContinuationCarrier: carrier,
+ messageSource: { connector: 'github-wait', label: 'GitHub Wait', meta: { waitContinuationCarrier: carrier } },
+ });
+ const dispatch = mock.fn(async () => ({ accepted: true, handle: {} }));
+ const run = bindRun(harness, dispatch);
+ await harness.queue.bindContinueCurrentIntentDurable('thread-1', 'user-1', admitted.entry.id, 'opus', {
+ requested: 'continue_current',
+ boundParentInvocationId: 'different-parent',
+ });
+ assert.equal(
+ (
+ await harness.processor.tryAutoAppendExactEntry({
+ threadId: 'thread-1',
+ userId: 'user-1',
+ entryId: admitted.entry.id,
+ targetCatId: 'opus',
+ })
+ ).outcome,
+ 'rejected',
+ );
+ assert.equal(dispatch.mock.calls.length, 0);
+ const bound = await harness.queue.bindContinueCurrentIntentDurable(
+ 'thread-1',
+ 'user-1',
+ admitted.entry.id,
+ 'opus',
+ {
+ requested: 'continue_current',
+ boundParentInvocationId: harness.invocationTracker.getExecutionId('thread-1', 'opus'),
+ },
+ );
+ assert.ok(bound);
+ assert.deepEqual(bound.from, from);
+ assert.deepEqual(bound.execution.waitContinuationCarrier, carrier);
+ const result = await harness.processor.tryAutoAppendExactEntry({
+ threadId: 'thread-1',
+ userId: 'user-1',
+ entryId: admitted.entry.id,
+ targetCatId: 'opus',
+ });
+ assert.equal(result.outcome, 'appended');
+ assert.equal(dispatch.mock.calls.length, 1);
+ assert.equal(harness.invocationTracker.has('thread-1', 'opus'), true);
+ assert.deepEqual((await harness.messageStore.getById(admitted.message.id)).from, from);
+ assert.deepEqual(activeInputs(harness), [admitted.message.id]);
+ assert.deepEqual(
+ (await harness.messageStore.getById(admitted.message.id)).source.meta.waitContinuationCarrier,
+ carrier,
+ );
+ });
+
it('publishes and binds accepted input immediately even when the carrier never reports consumption', async () => {
const harness = createHarness();
const admitted = await admit(harness);
diff --git a/packages/api/test/agent-router.test.js b/packages/api/test/agent-router.test.js
index f9e3d52eef..f3ea82344c 100644
--- a/packages/api/test/agent-router.test.js
+++ b/packages/api/test/agent-router.test.js
@@ -4232,3 +4232,46 @@ describe('F229: Concierge thread routing (duty-cat always takes priority)', () =
assert.deepEqual(result.targetCats, ['opus'], 'concierge duty cat wins over prior user @mention fallback');
});
});
+
+for (const [mode, text, targets] of [
+ ['serial', '@opus ordinary message', ['opus']],
+ ['parallel', '@opus @codex ordinary message', ['opus', 'codex']],
+]) {
+ test(`${mode}: ordinary delivery does not query the signal library`, async () => {
+ const { AgentRouter } = await import('../dist/domains/cats/services/agents/routing/AgentRouter.js');
+ const services = {
+ opus: createMockAgentService('opus', 'Opus response'),
+ codex: createMockAgentService('codex', 'Codex response'),
+ gemini: createMockAgentService('gemini', 'Gemini response'),
+ };
+ // Model a legacy injected dependency: normal delivery must not consult it,
+ // even if the library lookup would return content linked to this thread.
+ const lookup = mock.fn(async () => [
+ {
+ id: 'unrequested-article',
+ title: 'Unrequested article',
+ source: 'synthetic',
+ tier: 1,
+ contentSnippet: 'unrequested-library-content',
+ },
+ ]);
+ const router = new AgentRouter(
+ await migrateRouterOpts({
+ claudeService: services.opus,
+ codexService: services.codex,
+ geminiService: services.gemini,
+ registry: createMockRegistry(),
+ messageStore: createMockMessageStore(),
+ signalArticleLookup: lookup,
+ }),
+ );
+ const output = [];
+ for await (const message of router.route('user-1', text)) output.push(message);
+ for (const target of targets) {
+ assert.equal(services[target].invoke.mock.callCount(), 1);
+ assert.ok(output.some((message) => message.catId === target && message.type === 'done'));
+ assert.ok(!services[target].invoke.mock.calls[0].arguments[0].includes('unrequested-library-content'));
+ }
+ assert.equal(lookup.mock.callCount(), 0, 'normal delivery must perform no article lookup');
+ });
+}
diff --git a/packages/api/test/queue-api.test.js b/packages/api/test/queue-api.test.js
index 7a261a9c26..86666d6940 100644
--- a/packages/api/test/queue-api.test.js
+++ b/packages/api/test/queue-api.test.js
@@ -426,6 +426,33 @@ describe('Queue Management API', () => {
assert.equal(bound?.delivery.authorIntentByTarget?.opus?.boundParentInvocationId, 'turn-1');
});
+ for (const from of [
+ { kind: 'agent', catId: 'codex' },
+ { kind: 'system', service: 'github-wait' },
+ { kind: 'external', connectorId: 'github-wait', sender: { id: 'github-wait' } },
+ { kind: 'plugin', instanceId: 'notification-plugin' },
+ ]) {
+ it(`guides an exact active target for ${from.kind} input without changing source identity`, async () => {
+ const queued = await enqueueDurableEntry(deps.invocationQueue, { from });
+ deps.invocationTracker.has.mock.mockImplementation((_threadId, catId) => catId === 'opus');
+ deps.invocationTracker.getUserId.mock.mockImplementation(() => 'user-a');
+ deps.invocationTracker.getExecutionId.mock.mockImplementation(() => 'turn-1');
+ const res = await app.inject({
+ method: 'POST',
+ url: `/api/threads/t1/queue/${queued.entry.id}/continue`,
+ headers: { 'x-cat-cafe-user': 'user-a' },
+ payload: { targetCatId: 'opus' },
+ });
+ assert.equal(res.statusCode, 200);
+ assert.equal(res.json().outcome, 'appended');
+ assert.equal(deps.invocationTracker.cancel.mock.calls.length, 0);
+ const bound = deps.invocationQueue.getEntrySnapshot('t1', 'user-a', queued.entry.id);
+ assert.deepEqual(bound.from, from);
+ assert.equal(bound.delivery.authorIntentByTarget.opus.boundParentInvocationId, 'turn-1');
+ assert.equal(deps.queueProcessor.tryAutoAppendExactEntry.mock.calls[0].arguments[0].entryId, queued.entry.id);
+ });
+ }
+
it('POST /queue/:entryId/continue falls back to a new invocation when no reply is active', async () => {
const queued = await enqueueDurableEntry(deps.invocationQueue, { targetCats: [] });
diff --git a/packages/web/src/components/AppTooltip.tsx b/packages/web/src/components/AppTooltip.tsx
index 0fff2387fc..6d07e729dc 100644
--- a/packages/web/src/components/AppTooltip.tsx
+++ b/packages/web/src/components/AppTooltip.tsx
@@ -53,6 +53,8 @@ export function resetAppTooltipWarmState(): void {
}
interface AppTooltipProps {
+ /** Text rows can use a longer dwell than compact shell controls. */
+ delayMs?: number;
/** Read-only status controls can reveal their explanation on a tap. */
showOnClick?: boolean;
/** The control's name. Keep it short; it is also what a screen reader should hear on the control. */
@@ -82,6 +84,7 @@ function matchesFocusVisible(target: EventTarget | null): boolean {
}
export function AppTooltip({
+ delayMs = APP_TOOLTIP_DELAY_MS,
showOnClick = false,
label,
detail,
@@ -136,13 +139,13 @@ export function AppTooltip({
if (hideTimer.current) clearTimeout(hideTimer.current);
hideTimer.current = null;
if (showTimer.current) clearTimeout(showTimer.current);
- const wait = warm.active !== null || Date.now() < warm.until ? 0 : APP_TOOLTIP_DELAY_MS;
+ const wait = warm.active !== null || Date.now() < warm.until ? 0 : delayMs;
if (wait === 0) {
openNow();
return;
}
showTimer.current = setTimeout(openNow, wait);
- }, [disabled, openNow]);
+ }, [delayMs, disabled, openNow]);
const scheduleClose = useCallback(() => {
if (showTimer.current) clearTimeout(showTimer.current);
diff --git a/packages/web/src/components/AppendedInputReceipts.tsx b/packages/web/src/components/AppendedInputReceipts.tsx
index 226d869012..c9b3504622 100644
--- a/packages/web/src/components/AppendedInputReceipts.tsx
+++ b/packages/web/src/components/AppendedInputReceipts.tsx
@@ -6,6 +6,7 @@ import { useMeasuredOverflow } from '@/components/content-overflow/useMeasuredOv
import { ChevronIcon } from '@/components/hub-icons';
import type { CatData } from '@/hooks/useCatData';
import { useCoCreatorConfig } from '@/hooks/useCoCreatorConfig';
+import { catColorVar } from '@/lib/cat-slug';
import { resolveMessageSender } from '@/lib/resolve-sender';
import type { ChatMessage } from '@/stores/chat-types';
import { focusLineageMessage } from '@/utils/focusLineageMessage';
@@ -45,6 +46,7 @@ interface AppendedInputRowProps {
expanded: boolean;
onToggle: () => void;
response: ChatMessage;
+ color: string;
}
/** Only the exact delivered source × response can supply a read observation. */
@@ -56,13 +58,23 @@ export function appendedInputReadDisplay(source: ChatMessage, response: ChatMess
(ref) => ref.targetId === lifecycle.targetId && ref.statusMessageId === response.id,
) ?? [])
: [];
- const receipt = refs.length === 1 ? refs[0]?.inputRead : undefined;
- if (receipt?.status === 'read') return { state: 'read', label: '已读取', pulse: false };
- if (!receipt) return { state: 'unavailable', label: '已投递;读取状态不可用', pulse: false };
+ const ref = refs.length === 1 ? refs[0] : undefined;
+ const receipt = ref?.inputRead;
+ const label =
+ typeof ref?.dispatchedAt === 'number' && Number.isFinite(ref.dispatchedAt)
+ ? `投递于: ${formatReceiptTimestamp(ref.dispatchedAt)}`
+ : '已投递';
+ if (receipt?.status === 'read')
+ return {
+ state: 'read',
+ label: `读取于: ${formatReceiptTimestamp(receipt.at)}`,
+ pulse: false,
+ };
+ if (!receipt) return { state: 'unavailable', label, pulse: false };
const running = lifecycle?.kind === 'response' && lifecycle.status === 'processing';
return {
state: running ? 'pending' : 'unconfirmed',
- label: running ? '已投递;等待读取反馈' : '已投递;读取未确认',
+ label: running ? label : `${label}(未确认读取)`,
pulse: running,
};
}
@@ -71,51 +83,52 @@ export function appendedInputReadDisplay(source: ChatMessage, response: ChatMess
* One appended input: a single line that expands in place when it is actually truncated (the F269
* overflow rule shared with ExpandableProse), and a separate jump back to the original message.
*/
-function AppendedInputRow({ source, label, expanded, onToggle, response }: AppendedInputRowProps) {
+function AppendedInputRow({ source, label, expanded, onToggle, response, color }: AppendedInputRowProps) {
const contentId = useId();
const { ref, overflowing } = useMeasuredOverflow({ axis: 'inline', active: !expanded });
const content = source.content.trim() || '(无文字内容)';
const read = appendedInputReadDisplay(source, response);
+ const canToggle = overflowing || expanded;
return (
-
+ {content}}
+ delayMs={1_000}
+ multiline
+ side="top"
+ >
- {label}:
-
- {content}
-
- {(overflowing || expanded) && (
-
- )}