diff --git a/doc/DATABASE.md b/doc/DATABASE.md
index 1d21aa87f81..95a5df96245 100644
--- a/doc/DATABASE.md
+++ b/doc/DATABASE.md
@@ -331,6 +331,27 @@ chat handoff. An agent's external file selection must match the attachment's
company, task, agent, and originating run. Editing or recreating a work-product
record cannot reassign that authority to a later run.
+## Human-only task Internal notes
+
+`issue_internal_notes` stores notes separately from `issue_comments`. Its
+composite company/task foreign key prevents cross-company task attachment;
+`(issue_id, author_user_id, client_request_id)` makes a human save retry-safe.
+The company/task/timestamp/id index supports bounded, stable pagination without
+rounding database timestamp precision through JavaScript dates. Task deletion
+cascades notes. No historical ordinary comments are backfilled or reclassified.
+
+The dedicated API requires a signed-in human browser session and current active
+company membership; viewers can read but not write. Agent credentials, API keys
+and local-implicit actors are denied. Notes are deliberately excluded from task
+comments, search, references, agent context, activity bodies, wakes and external
+chat publications. A transactional, content-free activity row records creation
+once without an activity-stream/plugin publication. HTTP failure logging must
+not serialize note bodies. There is no edit/delete API in this initial slice.
+
+Notes are ordinary protected database data and are included in logical backups.
+This is not a secrets vault or a host-level sandbox: a process with direct
+database/filesystem access is outside the API/context isolation guarantee.
+
## Question-response delivery receipts
`issue_question_response_deliveries` is the retry-safe, content-free outbox for
diff --git a/doc/connections/SLACK-CEO-M2.md b/doc/connections/SLACK-CEO-M2.md
index 0696322a597..c6e00b6e4e6 100644
--- a/doc/connections/SLACK-CEO-M2.md
+++ b/doc/connections/SLACK-CEO-M2.md
@@ -55,9 +55,11 @@ discovery only: no editing, delegation, new tasks, PR or merge.
Expect one Connect Slack card in Slack and the canonical Paperclip task. The
link hands off through the authenticated task to personal OAuth. After the user
approves, the existing durable intent delivery resumes the original request.
-The answer keeps its original audience. A normal private Paperclip turn stays
-private; Reply via Slack opts in to that turn's Slack reply. Not now in Slack
-opens the board card for decline.
+The answer keeps its original audience. On legacy v1 tasks, the normal Paperclip
+composer stays internal and Reply via Slack opts in to that turn's Slack reply.
+On threaded v2 tasks, the normal composer shares request and answer by default;
+use the separate human-only Internal notes editor for non-agent notes. Not now
+in Slack opens the board card for decline.
`ensure_capability` supports `slack.read_channel` and `slack.read_thread` only.
Its public states are CONNECTED, AUTH_REQUIRED and UNAVAILABLE. Search is
@@ -145,10 +147,14 @@ after activation get one task/thread, a deduplicated eyes receipt and editable
working status. Both native and legacy ACPX runs can supply safe coarse progress;
tool updates are coalesced, and short runs may only show working then the answer.
-For new v2 bindings, **Reply via Slack** shares request and answer. The normal
-Paperclip composer is still internal, disclosed in the banner. Default sharing,
-human-only notes, explicit child-task linkage and historical screenshot research
-remain unfinished. This is not full M2b acceptance. Focused verification includes
+For new v2 bindings, the normal Paperclip composer now shares request and answer,
+with an explicit destination disclosure. Shared requests are text-only and use
+the existing durable outbox; the CEO starts only after Slack confirms delivery.
+Separate **Internal notes** are human-only, stored outside task comments and
+agent context. Earlier comments/drafts are not retroactively sent. The general
+Tasks list now includes Idle conversations without changing dashboard attention
+or execution behavior. Explicit child-task linkage and historical screenshot
+research remain unfinished. This is not full M2b acceptance. Earlier focused verification includes
291 tests in 11 suites and 34 selected integration checks, plus server/UI/adapter
typechecks and the UI build. The bot upgrade has now completed: live status shows
active v2, reactions enabled and cutoff `1790145683.516000`. Real threaded Slack
@@ -159,9 +165,15 @@ receipt, one threaded working message, and an in-place final answer. Eyes are
removed on completion. Subsequent live checks verified same-thread continuity on
TES-4, a separate TES-5 root, labelled Paperclip request plus answer through Reply
via Slack, and “CEO is using tools…” followed by a final answer in the same thread.
-The normal composer remains internal in this checkpoint. Next: Tasks-list
-visibility for Idle conversations, shared-default v2 composer and isolated
-human-only notes. No extra OAuth consent is needed for these UX fixes.
+At the subsequent September 23 checkpoint, migration 0285 was applied after a
+private backup. A live normal-composer request and its answer both reached the
+TES-4 thread, each in one publication attempt; the task returned to Idle.
+TES-4/TES-5 are visible in Tasks. A saved Internal-note canary did not enter
+comments, Slack publications, wakes or activity, including after that shared run.
+186 focused tests, server/UI typechecks, token gates, UI and Storybook builds
+passed. Full visual-baseline acceptance remains unrun. Notes are in database
+backups; their API/context isolation is not a sandbox against host/database
+administrators. No extra OAuth consent was needed for these UX fixes.
Implementation, coding-worker approval, artifacts, GitHub and merge are later
milestones. Discovery guidance is not a security boundary for every CEO tool.
diff --git a/doc/plans/2026-09-22-slack-ceo-threaded-research.md b/doc/plans/2026-09-22-slack-ceo-threaded-research.md
index cb3634b365e..bf53952f7ce 100644
--- a/doc/plans/2026-09-22-slack-ceo-threaded-research.md
+++ b/doc/plans/2026-09-22-slack-ceo-threaded-research.md
@@ -1,7 +1,7 @@
# Slack CEO M2b: threaded, repository-backed research
Status: narrow threaded-DM preview deployed and activated; live roots, follow-up,
-explicit shared reply and coarse tool progress verified. Full
+shared normal composer, isolated human-only notes and coarse tool progress verified. Full
M2b remains incomplete. Continue `codex/slack-ceo-poc1`,
not the independent `codex/connection-capability-092226` checkout. Starting
commit: `91339784ce9a740c149b34f013a1e29e7e7febdc`.
@@ -32,12 +32,17 @@ runner now emits a payload-free tool activity signal, coalesced at 20 seconds.
Short turns can go straight from working to the final answer. Never promise
individual grep/command names or expose raw arguments, reasoning or paths.
-For v2, explicit **Reply via Slack** shares the request before waking the CEO,
-then shares the answer. The banner explicitly calls this a preview: normal
-Paperclip composer messages still remain internal. Shared-default composer plus
-isolated human-only notes, explicit child-task announcement/binding, historical
-scan storage and screenshot inspection remain future M2b work. Their absence
-does not prevent the narrow new-DM test, but this is not full M2b acceptance.
+For v2, the normal Paperclip composer now uses the existing shared-request outbox:
+the labelled request reaches the exact Slack thread before the CEO wakes, and
+its answer returns there. The composer discloses sharing and is text-only; task
+file attachments are not yet supported. Its new, audience-specific draft key
+does not restore earlier internal drafts. The redundant v2 **Reply via Slack**
+box is removed; legacy TES-2 retains its original explicit-reply behavior.
+Separate **Internal notes** are available to signed-in company members, never
+through agent/API-key access or normal task context/search/publication. Existing
+ordinary comments are not migrated into this store and remain agent-readable;
+they are not retroactively posted to Slack. Explicit child-task binding,
+historical scan storage and screenshot inspection remain future M2b work.
Verification: 291 tests across 11 focused suites; 34 selected chat integration
checks; server/UI/adapter-utils TypeScript checks; token gates and UI build pass.
@@ -88,12 +93,53 @@ retry and created TES-5 after the admission correction.
13 channel posts and 12 replies across three threads; this is reported coverage,
not an independent audit of those upstream reads. Attachments were not examined.
-Remaining immediate UX fixes: keep Idle Slack conversations discoverable in the
-Tasks list without making them active work; route the normal composer through the
-existing shared-request outbox for v2 tasks; add separately stored human-only
-Internal notes. Never retroactively publish existing internal comments. Historical
-channel reading, screenshots and explicit child-task thread linkage remain later
-work; the narrow preview does not claim those capabilities.
+These checks exposed three immediate UX fixes, completed in the following checkpoint.
+
+### Shared composer, Internal notes and Idle visibility (September 23, 00:28 PDT)
+
+Pushed the prior threaded work as `fbcae3cd88da9decbea089e2c37eccc57f4145b1` to
+`origin/codex/slack-ceo-poc1`, without a PR, before starting these fixes. The new
+UX changes are local and running on the isolated pilot. A private logical backup
+preceded generated migration `0285_abandoned_mentallo.sql` and server restart.
+
+- The general Tasks list opts into Idle Slack conversations. Existing dashboard,
+ review-attention and execution counts retain their prior default exclusions.
+ Live Tasks inspection shows TES-2, TES-4 and TES-5 as Idle.
+- Normal v2 composer submission uses the durable board-reply path, including
+ request IDs, confirmed-send-before-wake and authoritative audience checks.
+ Ambiguous responses preserve the request identity for retry; they never fall
+ back to an ordinary internal comment. File attachments/reassignment are rejected.
+- `issue_internal_notes` is a separate company/task-scoped table with idempotent
+ human creation and paginated reads. Only an authenticated browser session plus
+ current active membership can read it; viewers cannot write. Agents, board API
+ keys and local-implicit actors cannot use the endpoint. Notes create no comment,
+ task mutation, wake, activity-body copy, SSE payload or chat publication. One
+ transactional content-free audit row records creation without a live event. HTTP
+ logs/errors use content-free projections. This is an API/context boundary, not
+ isolation from a host administrator or process with direct database access;
+ database backups contain notes. Do not use this feature as a secrets vault.
+
+Live normal-composer test on TES-4 at 00:26 PDT: run
+`4824bbc0-3c38-4bc7-83f0-48bbf2d518fd` succeeded and the task returned to Idle.
+Request publication `6a449038-d7a7-4e0c-8041-27845b264b0c` and final publication
+`211b11b1-5388-482e-9699-8d3f071082bd` each succeeded in one attempt. The real
+Slack UI showed the labelled request and “Shared composer test passed.” under
+the original TES-4 root. A harmless Internal-note canary saved beforehand remained
+absent from comments, publications, wakes and activity after that shared run.
+Saving it alone did not start any run.
+
+Verification: 186 tests across eight focused suites passed, including real
+temporary-database note isolation, tenant boundaries, viewer/agent denial,
+idempotence, timestamp-precise pagination, ordinary composer retries and lifecycle
+regressions. Server/UI TypeScript, migration checks, token gates, UI build and
+Storybook build passed. Live UI inspection covered the notes dialog, shared
+composer and Tasks list. Full visual-baseline acceptance was not run; the broader
+baseline failures below remain. This is not a full-release or full-M2b claim.
+
+Next: durable explicit new-task announcement/binding, then resumable historical
+reads and actual screenshot inspection, followed by pinned read-only repository
+assessment. TES-3 remains unlinked; do not infer authority from its prose source
+link. No implementation, delegation, PR or merge workflow is enabled here.
## Current evidence and immediate repair
diff --git a/packages/db/src/migrations/0285_abandoned_mentallo.sql b/packages/db/src/migrations/0285_abandoned_mentallo.sql
new file mode 100644
index 00000000000..f24c9011e65
--- /dev/null
+++ b/packages/db/src/migrations/0285_abandoned_mentallo.sql
@@ -0,0 +1,14 @@
+CREATE TABLE "issue_internal_notes" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "company_id" uuid NOT NULL,
+ "issue_id" uuid NOT NULL,
+ "author_user_id" text NOT NULL,
+ "client_request_id" uuid NOT NULL,
+ "body" text NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT "issue_internal_notes_request_uq" UNIQUE("issue_id","author_user_id","client_request_id")
+);
+--> statement-breakpoint
+ALTER TABLE "issue_internal_notes" ADD CONSTRAINT "issue_internal_notes_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "issue_internal_notes" ADD CONSTRAINT "issue_internal_notes_issue_company_fk" FOREIGN KEY ("company_id","issue_id") REFERENCES "public"."issues"("company_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "issue_internal_notes_issue_idx" ON "issue_internal_notes" USING btree ("company_id","issue_id","created_at","id");
\ No newline at end of file
diff --git a/packages/db/src/migrations/meta/0280_snapshot.json b/packages/db/src/migrations/meta/0285_snapshot.json
similarity index 98%
rename from packages/db/src/migrations/meta/0280_snapshot.json
rename to packages/db/src/migrations/meta/0285_snapshot.json
index ea3ae5e50dd..6c9aa9ce37f 100644
--- a/packages/db/src/migrations/meta/0280_snapshot.json
+++ b/packages/db/src/migrations/meta/0285_snapshot.json
@@ -1,6 +1,6 @@
{
- "id": "861a2815-1418-4c71-a77b-d0e2e9566413",
- "prevId": "4770ad6d-2cdc-45c4-af89-c300fd134d91",
+ "id": "e1655800-f133-4bf3-b810-a100641abcac",
+ "prevId": "8a038eb1-fe99-4de5-9608-3f4c7c2b53e5",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -5675,6 +5675,25 @@
"notNull": true,
"default": "''"
},
+ "binding_mode": {
+ "name": "binding_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'legacy'"
+ },
+ "origin_principal_id": {
+ "name": "origin_principal_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin_user_id": {
+ "name": "origin_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
"session_generation": {
"name": "session_generation",
"type": "integer",
@@ -5701,6 +5720,12 @@
"notNull": true,
"default": false
},
+ "communication_guidance": {
+ "name": "communication_guidance",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
"state": {
"name": "state",
"type": "text",
@@ -5825,6 +5850,21 @@
"onDelete": "restrict",
"onUpdate": "no action"
},
+ "chat_conversations_origin_principal_fk": {
+ "name": "chat_conversations_origin_principal_fk",
+ "tableFrom": "chat_conversations",
+ "tableTo": "chat_external_principals",
+ "columnsFrom": [
+ "company_id",
+ "origin_principal_id"
+ ],
+ "columnsTo": [
+ "company_id",
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
"chat_conversations_company_issue_fk": {
"name": "chat_conversations_company_issue_fk",
"tableFrom": "chat_conversations",
@@ -5887,6 +5927,14 @@
"chat_conversations_state_check": {
"name": "chat_conversations_state_check",
"value": "\"chat_conversations\".\"state\" in ('active', 'waiting', 'completed', 'unavailable', 'endpoint_removed')"
+ },
+ "chat_conversations_binding_mode_check": {
+ "name": "chat_conversations_binding_mode_check",
+ "value": "\"chat_conversations\".\"binding_mode\" in ('legacy', 'slack_dm_thread_v2')"
+ },
+ "chat_conversations_slack_thread_binding_check": {
+ "name": "chat_conversations_slack_thread_binding_check",
+ "value": "\"chat_conversations\".\"binding_mode\" = 'legacy' or (\n \"chat_conversations\".\"is_direct_message\" and \"chat_conversations\".\"session_generation\" = 1\n and \"chat_conversations\".\"origin_principal_id\" is not null and \"chat_conversations\".\"origin_user_id\" is not null\n and \"chat_conversations\".\"external_conversation_id\" ~ '^D[A-Z0-9]+$'\n and \"chat_conversations\".\"external_thread_id\" ~ '^slack:D[A-Z0-9]+:[0-9]{1,12}[.][0-9]{1,6}$'\n and split_part(\"chat_conversations\".\"external_thread_id\", ':', 2) = \"chat_conversations\".\"external_conversation_id\")"
}
},
"isRLSEnabled": false
@@ -6611,6 +6659,13 @@
"primaryKey": false,
"notNull": false
},
+ "communication_instructions": {
+ "name": "communication_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
"allow_direct_messages": {
"name": "allow_direct_messages",
"type": "boolean",
@@ -8096,6 +8151,519 @@
},
"isRLSEnabled": false
},
+ "public.chat_github_configurations": {
+ "name": "chat_github_configurations",
+ "schema": "",
+ "columns": {
+ "endpoint_id": {
+ "name": "endpoint_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "configuration": {
+ "name": "configuration",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by_user_id": {
+ "name": "updated_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "chat_github_configurations_company_id_endpoint_id_chat_endpoints_company_id_id_fk": {
+ "name": "chat_github_configurations_company_id_endpoint_id_chat_endpoints_company_id_id_fk",
+ "tableFrom": "chat_github_configurations",
+ "tableTo": "chat_endpoints",
+ "columnsFrom": [
+ "company_id",
+ "endpoint_id"
+ ],
+ "columnsTo": [
+ "company_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "chat_github_config_revision_check": {
+ "name": "chat_github_config_revision_check",
+ "value": "\"chat_github_configurations\".\"revision\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.chat_github_registrations": {
+ "name": "chat_github_registrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint_id": {
+ "name": "endpoint_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_hash": {
+ "name": "state_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trusted_origin": {
+ "name": "trusted_origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "consumed_at": {
+ "name": "consumed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "chat_github_registration_state_uq": {
+ "name": "chat_github_registration_state_uq",
+ "columns": [
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chat_github_registration_endpoint_idx": {
+ "name": "chat_github_registration_endpoint_idx",
+ "columns": [
+ {
+ "expression": "endpoint_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_github_registrations_company_id_endpoint_id_chat_endpoints_company_id_id_fk": {
+ "name": "chat_github_registrations_company_id_endpoint_id_chat_endpoints_company_id_id_fk",
+ "tableFrom": "chat_github_registrations",
+ "tableTo": "chat_endpoints",
+ "columnsFrom": [
+ "company_id",
+ "endpoint_id"
+ ],
+ "columnsTo": [
+ "company_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "chat_github_registration_status_check": {
+ "name": "chat_github_registration_status_check",
+ "value": "\"chat_github_registrations\".\"status\" in ('pending', 'exchanging', 'completed', 'failed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.chat_github_reviews": {
+ "name": "chat_github_reviews",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint_id": {
+ "name": "endpoint_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issue_id": {
+ "name": "issue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pull_number": {
+ "name": "pull_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "head_sha": {
+ "name": "head_sha",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "delivery_id": {
+ "name": "delivery_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "configuration_revision": {
+ "name": "configuration_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "policy_snapshot": {
+ "name": "policy_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event": {
+ "name": "event",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "assessment": {
+ "name": "assessment",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "conclusion": {
+ "name": "conclusion",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "check_id": {
+ "name": "check_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "check_url": {
+ "name": "check_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary_id": {
+ "name": "summary_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary_url": {
+ "name": "summary_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "publication_receipts": {
+ "name": "publication_receipts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "chat_github_reviews_delivery_uq": {
+ "name": "chat_github_reviews_delivery_uq",
+ "columns": [
+ {
+ "expression": "endpoint_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "delivery_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chat_github_reviews_pull_idx": {
+ "name": "chat_github_reviews_pull_idx",
+ "columns": [
+ {
+ "expression": "endpoint_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pull_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chat_github_reviews_task_idx": {
+ "name": "chat_github_reviews_task_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "issue_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_github_reviews_run_id_heartbeat_runs_id_fk": {
+ "name": "chat_github_reviews_run_id_heartbeat_runs_id_fk",
+ "tableFrom": "chat_github_reviews",
+ "tableTo": "heartbeat_runs",
+ "columnsFrom": [
+ "run_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "chat_github_reviews_company_id_endpoint_id_chat_endpoints_company_id_id_fk": {
+ "name": "chat_github_reviews_company_id_endpoint_id_chat_endpoints_company_id_id_fk",
+ "tableFrom": "chat_github_reviews",
+ "tableTo": "chat_endpoints",
+ "columnsFrom": [
+ "company_id",
+ "endpoint_id"
+ ],
+ "columnsTo": [
+ "company_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_github_reviews_company_id_issue_id_issues_company_id_id_fk": {
+ "name": "chat_github_reviews_company_id_issue_id_issues_company_id_id_fk",
+ "tableFrom": "chat_github_reviews",
+ "tableTo": "issues",
+ "columnsFrom": [
+ "company_id",
+ "issue_id"
+ ],
+ "columnsTo": [
+ "company_id",
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "chat_github_reviews_company_id_uq": {
+ "name": "chat_github_reviews_company_id_uq",
+ "nullsNotDistinct": false,
+ "columns": [
+ "company_id",
+ "id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "chat_github_review_pull_number_check": {
+ "name": "chat_github_review_pull_number_check",
+ "value": "\"chat_github_reviews\".\"pull_number\" > 0"
+ },
+ "chat_github_review_state_check": {
+ "name": "chat_github_review_state_check",
+ "value": "\"chat_github_reviews\".\"state\" in ('queued', 'running', 'completed', 'incomplete', 'error', 'superseded', 'manual_required')"
+ }
+ },
+ "isRLSEnabled": false
+ },
"public.chat_teams_file_transfers": {
"name": "chat_teams_file_transfers",
"schema": "",
@@ -25752,6 +26320,136 @@
},
"isRLSEnabled": false
},
+ "public.issue_internal_notes": {
+ "name": "issue_internal_notes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issue_id": {
+ "name": "issue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_user_id": {
+ "name": "author_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_request_id": {
+ "name": "client_request_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "issue_internal_notes_issue_idx": {
+ "name": "issue_internal_notes_issue_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "issue_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "issue_internal_notes_company_id_companies_id_fk": {
+ "name": "issue_internal_notes_company_id_companies_id_fk",
+ "tableFrom": "issue_internal_notes",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "issue_internal_notes_issue_company_fk": {
+ "name": "issue_internal_notes_issue_company_fk",
+ "tableFrom": "issue_internal_notes",
+ "tableTo": "issues",
+ "columnsFrom": [
+ "company_id",
+ "issue_id"
+ ],
+ "columnsTo": [
+ "company_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "issue_internal_notes_request_uq": {
+ "name": "issue_internal_notes_request_uq",
+ "nullsNotDistinct": false,
+ "columns": [
+ "issue_id",
+ "author_user_id",
+ "client_request_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
"public.issue_labels": {
"name": "issue_labels",
"schema": "",
@@ -37416,6 +38114,26 @@
"notNull": true,
"default": true
},
+ "setup_pending": {
+ "name": "setup_pending",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "archived": {
+ "name": "archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "last_webhook_delivery": {
+ "name": "last_webhook_delivery",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
"cron_expression": {
"name": "cron_expression",
"type": "text",
@@ -37677,6 +38395,100 @@
"checkConstraints": {},
"isRLSEnabled": false
},
+ "public.routine_webhook_test_receipts": {
+ "name": "routine_webhook_test_receipts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trigger_id": {
+ "name": "trigger_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "delivery_key_hash": {
+ "name": "delivery_key_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "routine_webhook_test_receipts_delivery_uq": {
+ "name": "routine_webhook_test_receipts_delivery_uq",
+ "columns": [
+ {
+ "expression": "trigger_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "delivery_key_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "routine_webhook_test_receipts_company_id_companies_id_fk": {
+ "name": "routine_webhook_test_receipts_company_id_companies_id_fk",
+ "tableFrom": "routine_webhook_test_receipts",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "routine_webhook_test_receipts_trigger_id_routine_triggers_id_fk": {
+ "name": "routine_webhook_test_receipts_trigger_id_routine_triggers_id_fk",
+ "tableFrom": "routine_webhook_test_receipts",
+ "tableTo": "routine_triggers",
+ "columnsFrom": [
+ "trigger_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
"public.routines": {
"name": "routines",
"schema": "",
diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json
index 9a7076a356c..cfc142f1a7c 100644
--- a/packages/db/src/migrations/meta/_journal.json
+++ b/packages/db/src/migrations/meta/_journal.json
@@ -1982,6 +1982,13 @@
"when": 1790143464911,
"tag": "0284_amusing_bloodaxe",
"breakpoints": true
+ },
+ {
+ "idx": 285,
+ "version": "7",
+ "when": 1790147738042,
+ "tag": "0285_abandoned_mentallo",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts
index 81ea3bc33bb..ae9ec9e2eb5 100644
--- a/packages/db/src/schema/index.ts
+++ b/packages/db/src/schema/index.ts
@@ -41,6 +41,7 @@ export { projectGoals } from "./project_goals.js";
export { goals } from "./goals.js";
export { folders } from "./folders.js";
export { issues } from "./issues.js";
+export { issueInternalNotes } from "./issue_internal_notes.js";
export { issueWatchdogs } from "./issue_watchdogs.js";
export { issuePlanDecompositions } from "./issue_plan_decompositions.js";
export { issueRecoveryActions } from "./issue_recovery_actions.js";
diff --git a/packages/db/src/schema/issue_internal_notes.ts b/packages/db/src/schema/issue_internal_notes.ts
new file mode 100644
index 00000000000..464861342a1
--- /dev/null
+++ b/packages/db/src/schema/issue_internal_notes.ts
@@ -0,0 +1,19 @@
+import { foreignKey, index, pgTable, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
+import { companies } from "./companies.js";
+import { issues } from "./issues.js";
+
+// Separate from issue_comments: never fed into agent context, search,
+// references, activity bodies, wakes or external chat publications.
+export const issueInternalNotes = pgTable("issue_internal_notes", {
+ id: uuid("id").primaryKey().defaultRandom(),
+ companyId: uuid("company_id").notNull().references(() => companies.id),
+ issueId: uuid("issue_id").notNull(),
+ authorUserId: text("author_user_id").notNull(),
+ clientRequestId: uuid("client_request_id").notNull(),
+ body: text("body").notNull(),
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
+}, table => ({
+ issueCompanyFk: foreignKey({ columns: [table.companyId, table.issueId], foreignColumns: [issues.companyId, issues.id], name: "issue_internal_notes_issue_company_fk" }).onDelete("cascade"),
+ requestUq: unique("issue_internal_notes_request_uq").on(table.issueId, table.authorUserId, table.clientRequestId),
+ issueIdx: index("issue_internal_notes_issue_idx").on(table.companyId, table.issueId, table.createdAt, table.id),
+}));
diff --git a/server/src/__tests__/http-log-redaction.test.ts b/server/src/__tests__/http-log-redaction.test.ts
index a16e56e69a5..8644f339e55 100644
--- a/server/src/__tests__/http-log-redaction.test.ts
+++ b/server/src/__tests__/http-log-redaction.test.ts
@@ -13,6 +13,8 @@ import { createHttpLogger } from "../middleware/logger.js";
describe("HTTP logger redaction", () => {
it.each([
+ { method: "POST", path: "/api/issues/private-url-canary/internal-notes" },
+ { method: "GET", path: "/api/issues/private-url-canary/internal-notes?before=private-url-canary" },
{ method: "POST", path: "/api/routine-triggers/public/private-url-canary/fire" },
{ method: "PUT", path: "/api/routine-triggers/public/private-url-canary/fire" },
@@ -98,7 +100,7 @@ describe("HTTP logger redaction", () => {
const log = JSON.parse(output.trim());
expect(log.req).toMatchObject({
method,
- url: path.includes("routine-triggers") ? "/api/routine-triggers/public/:publicId/fire" : "/api/chat-webhooks/:publicId/:provider",
+ url: path.includes("internal-notes") ? "/api/issues/:id/internal-notes" : path.includes("routine-triggers") ? "/api/routine-triggers/public/:publicId/fire" : "/api/chat-webhooks/:publicId/:provider",
});
expect(log.reqBody).toBe("[REDACTED]");
expect(log.err.message).toBe("Chat webhook request failed");
diff --git a/server/src/__tests__/issue-internal-notes.test.ts b/server/src/__tests__/issue-internal-notes.test.ts
new file mode 100644
index 00000000000..006b2065450
--- /dev/null
+++ b/server/src/__tests__/issue-internal-notes.test.ts
@@ -0,0 +1,86 @@
+import { randomUUID } from "node:crypto";
+import express from "express";
+import request from "supertest";
+import { eq } from "drizzle-orm";
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+import { activityLog, agentWakeupRequests, chatActions, chatPublications, companies, companyMemberships, createDb,
+ issueComments, issueInternalNotes, issues } from "@paperclipai/db";
+import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
+import { issueInternalNoteRoutes } from "../routes/issue-internal-notes.js";
+import { errorHandler } from "../middleware/error-handler.js";
+import { issueService } from "../services/issues.js";
+import { companySearchService } from "../services/company-search.js";
+import { companySearchQuerySchema } from "@paperclipai/shared";
+
+const support = await getEmbeddedPostgresTestSupport();
+(support.supported ? describe : describe.skip)("human-only Internal notes", () => {
+ let database: Awaited Thread preview: use Reply via Slack to share a request and the CEO’s answer in this thread. The normal Paperclip composer does not send messages to Slack yet.
- Ordinary board comments remain Paperclip-only. + {binding.bindingMode === "slack_dm_thread_v2" ? "This sends an update without asking the CEO to respond. Use the task composer for a conversation." : "Ordinary board comments remain Paperclip-only."}