@agent-team-foundation/first-tree-hub 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,59 +6,63 @@
6
6
  -- 1. Drop `agent_tokens` (table + FK cascade).
7
7
  -- 2. Add `clients.user_id` (nullable) — owning user of the physical client.
8
8
  -- 3. Add `agents.client_id` (nullable FK) — pin an agent to the client that
9
- -- runs it. `client_id` is backfilled from `agent_presence` and is
10
- -- required for every non-human agent (enforced in the service layer per
11
- -- CLAUDE.md "integrity in service layer"; no DB CHECK/trigger).
9
+ -- runs it. `client_id` is backfilled from `agent_presence`. Rows that
10
+ -- cannot be backfilled stay NULL and bind on first WS connect (see
11
+ -- `api/agent/ws-client.ts` first-bind path). Originally this migration
12
+ -- raised an exception when any non-human agent was unbacked — that
13
+ -- gated startup on a data state the operator usually can't fix until
14
+ -- the server is up. Relaxed to NOTICE so the loop is broken; runtime
15
+ -- enforcement (Rule R-RUN in `services/agent.ts` + `agent-selector.ts`)
16
+ -- still rejects unclaimed agents on the request path.
12
17
  -- 4. Make `agents.manager_id` NOT NULL after backfilling the first admin
13
18
  -- member of each org onto the unmanaged rows.
14
19
  --
15
20
  -- There is no compatibility layer: operators stop SDK/CLI processes, run
16
21
  -- `db:migrate`, then re-login via `first-tree-hub connect`. See the proposal
17
22
  -- "unified-user-token.20260417" for the full upgrade runbook.
23
+ --
24
+ -- NOTE: Do NOT wrap this file in BEGIN;/COMMIT;. The Drizzle migrator already
25
+ -- runs every pending migration inside a single outer transaction, so a nested
26
+ -- BEGIN raises WARNING 25001 and the inner COMMIT prematurely closes the
27
+ -- outer transaction — which prevents the migration hash from being recorded
28
+ -- and causes the server to loop through the same failure on every restart.
18
29
 
19
- BEGIN;
20
-
21
- -- ---------------------------------------------------------------------------
22
- -- 1. Drop agent_tokens (FK cascade handles row removal).
23
- -- ---------------------------------------------------------------------------
24
30
  DROP TABLE IF EXISTS "agent_tokens";
31
+ --> statement-breakpoint
25
32
 
26
33
  -- ---------------------------------------------------------------------------
27
- -- 2. clients.user_id — nullable FK to users(id).
28
- -- Nullable so legacy rows (created before handshake auth) keep existing;
29
- -- the WS handshake claims them on first re-register under a JWT.
34
+ -- clients.user_id — nullable FK to users(id).
35
+ -- Nullable so legacy rows (created before handshake auth) keep existing;
36
+ -- the WS handshake claims them on first re-register under a JWT.
30
37
  -- ---------------------------------------------------------------------------
31
38
  ALTER TABLE "clients"
32
39
  ADD COLUMN IF NOT EXISTS "user_id" text REFERENCES "users"("id") ON DELETE SET NULL;
40
+ --> statement-breakpoint
33
41
 
34
42
  CREATE INDEX IF NOT EXISTS "idx_clients_user" ON "clients" ("user_id");
43
+ --> statement-breakpoint
35
44
 
36
45
  -- ---------------------------------------------------------------------------
37
- -- 3. agents.client_id — pin an agent to the physical client that runs it.
38
- -- Backfill in two steps:
39
- -- a. Copy the most recent bind from agent_presence, if any.
40
- -- b. For non-human agents with no bind history, attempt to pick the
41
- -- first client in the agent's org. If none exists we intentionally
42
- -- leave the row NULL; the service layer refuses runtime bind while
43
- -- `client_id IS NULL` so operators notice and fix it.
46
+ -- agents.client_id — pin an agent to the physical client that runs it.
44
47
  -- ---------------------------------------------------------------------------
45
48
  ALTER TABLE "agents"
46
49
  ADD COLUMN IF NOT EXISTS "client_id" text REFERENCES "clients"("id") ON DELETE RESTRICT;
50
+ --> statement-breakpoint
47
51
 
48
- -- 3a. Copy last-bound client from agent_presence.
52
+ -- Copy last-bound client from agent_presence.
49
53
  UPDATE "agents" a
50
54
  SET "client_id" = ap."client_id"
51
55
  FROM "agent_presence" ap
52
56
  WHERE ap."agent_id" = a."uuid"
53
57
  AND ap."client_id" IS NOT NULL
54
58
  AND a."client_id" IS NULL;
59
+ --> statement-breakpoint
55
60
 
56
- -- 3b. Fail loudly if any non-deleted non-human agent is missing a client_id
57
- -- after 3a. These rows would pass migration but surface at runtime as
58
- -- `WRONG_CLIENT` / `assertClientOwner` 404 which looks like "installed
59
- -- but broken" to the operator. Mirrors 4c's orphan-count pattern. Admins
60
- -- must either soft-delete the orphans or re-register their client via
61
- -- `first-tree-hub connect` and manually UPDATE before retrying.
61
+ -- Surface (do not block) any non-deleted non-human agent still missing a
62
+ -- client_id after the backfill. They will sit in an "unclaimed" state until
63
+ -- a client connects via WS and the first-bind path in
64
+ -- `api/agent/ws-client.ts` claims them. Runtime guards reject HTTP / WS
65
+ -- requests for those agents in the meantime.
62
66
  DO $$
63
67
  DECLARE
64
68
  unpinned_count integer;
@@ -70,31 +74,33 @@ BEGIN
70
74
  AND "status" <> 'deleted';
71
75
 
72
76
  IF unpinned_count > 0 THEN
73
- RAISE EXCEPTION
77
+ RAISE NOTICE
74
78
  'unified-user-token migration: % non-human agents have no client_id after backfill; '
75
- 're-register their client via `first-tree-hub connect` (which will update agent_presence) '
76
- 'or soft-delete the orphan agents, then retry',
79
+ 'they will be claimed on first WS bind (see ws-client.ts first-bind path)',
77
80
  unpinned_count;
78
81
  END IF;
79
82
  END $$;
83
+ --> statement-breakpoint
80
84
 
81
85
  CREATE INDEX IF NOT EXISTS "idx_agents_client" ON "agents" ("client_id");
86
+ --> statement-breakpoint
82
87
 
83
88
  -- ---------------------------------------------------------------------------
84
- -- 4. agents.manager_id — backfill then enforce NOT NULL.
85
- -- Human agents own their members row, so self-assign via members.
86
- -- Non-human agents get the first admin member in their org.
89
+ -- agents.manager_id — backfill then enforce NOT NULL.
90
+ -- Human agents own their members row, so self-assign via members.
91
+ -- Non-human agents get the first admin member in their org.
87
92
  -- ---------------------------------------------------------------------------
88
93
 
89
- -- 4a. Human agents: self-assign to their own member row.
94
+ -- Human agents: self-assign to their own member row.
90
95
  UPDATE "agents" a
91
96
  SET "manager_id" = m."id"
92
97
  FROM "members" m
93
98
  WHERE m."agent_id" = a."uuid"
94
99
  AND a."manager_id" IS NULL
95
100
  AND a."type" = 'human';
101
+ --> statement-breakpoint
96
102
 
97
- -- 4b. Non-human agents: first admin in org, ordered by created_at asc.
103
+ -- Non-human agents: first admin in org, ordered by created_at asc.
98
104
  UPDATE "agents" a
99
105
  SET "manager_id" = m."id"
100
106
  FROM (
@@ -107,9 +113,9 @@ FROM (
107
113
  ) m
108
114
  WHERE a."organization_id" = m."organization_id"
109
115
  AND a."manager_id" IS NULL;
116
+ --> statement-breakpoint
110
117
 
111
- -- 4c. Fail loudly if any agent still lacks a manager. Admin must intervene
112
- -- before the migration can complete.
118
+ -- Fail loudly if any agent still lacks a manager.
113
119
  DO $$
114
120
  DECLARE
115
121
  orphan_count integer;
@@ -126,23 +132,23 @@ BEGIN
126
132
  orphan_count;
127
133
  END IF;
128
134
  END $$;
135
+ --> statement-breakpoint
129
136
 
130
137
  ALTER TABLE "agents"
131
138
  ALTER COLUMN "manager_id" SET NOT NULL;
139
+ --> statement-breakpoint
132
140
 
133
- -- 4d. Recreate the manager_id FK as DEFERRABLE INITIALLY DEFERRED so the
134
- -- bootstrap path for a new human agent + member row can run inside a
135
- -- single transaction. The two rows reference each other (agents.manager_id
136
- -- → members.id, members.agent_id → agents.uuid); without deferred FKs the
137
- -- first INSERT always fails the sibling constraint. Manager reassignment
138
- -- (member.ts::deleteMember) also benefits: we can move every agent in one
139
- -- pass without fighting constraint order.
141
+ -- Recreate the manager_id FK as DEFERRABLE INITIALLY DEFERRED so the
142
+ -- bootstrap path for a new human agent + member row can run inside a
143
+ -- single transaction. The two rows reference each other (agents.manager_id
144
+ -- → members.id, members.agent_id → agents.uuid); without deferred FKs the
145
+ -- first INSERT always fails the sibling constraint.
140
146
  ALTER TABLE "agents"
141
147
  DROP CONSTRAINT IF EXISTS "agents_manager_id_fkey";
148
+ --> statement-breakpoint
149
+
142
150
  ALTER TABLE "agents"
143
151
  ADD CONSTRAINT "agents_manager_id_fkey"
144
152
  FOREIGN KEY ("manager_id") REFERENCES "members"("id")
145
153
  ON DELETE SET NULL
146
154
  DEFERRABLE INITIALLY DEFERRED;
147
-
148
- COMMIT;
@@ -173,7 +173,7 @@ const updateAgentSchema = z.object({
173
173
  visibility: agentVisibilitySchema.optional(),
174
174
  metadata: z.record(z.string(), z.unknown()).optional(),
175
175
  managerId: z.string().nullable().optional(),
176
- clientId: z.string().optional()
176
+ clientId: z.string().min(1).max(100).nullable().optional()
177
177
  });
178
178
  z.object({
179
179
  uuid: z.string(),
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-DW7aIpmE.mjs";
2
- import { A as SdkError, C as ensurePostgres, D as createOwner, E as ClientRuntime, O as hasUser, S as status, T as stopPostgres, _ as checkServerHealth, a as formatCheckReport, b as printResults, c as onboardCreate, d as checkAgentConfigs, f as checkClientConfig, g as checkServerConfig, h as checkNodeVersion, i as promptMissingFields, k as FirstTreeHubSDK, m as checkDocker, n as isInteractive, p as checkDatabase, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerReachable, w as isDockerAvailable, x as blank, y as checkWebSocket } from "./core-RXUUKkCO.mjs";
3
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-BZ8pnMrQ.mjs";
1
+ import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-DNL1cEwv.mjs";
2
+ import { A as SdkError, C as ensurePostgres, D as createOwner, E as ClientRuntime, O as hasUser, S as status, T as stopPostgres, _ as checkServerHealth, a as formatCheckReport, b as printResults, c as onboardCreate, d as checkAgentConfigs, f as checkClientConfig, g as checkServerConfig, h as checkNodeVersion, i as promptMissingFields, k as FirstTreeHubSDK, m as checkDocker, n as isInteractive, p as checkDatabase, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerReachable, w as isDockerAvailable, x as blank, y as checkWebSocket } from "./core-B10jgThe.mjs";
3
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-BoMJHlOv.mjs";
4
4
  export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, hasUser, isDockerAvailable, isInteractive, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveServerUrl, runMigrations, startServer, status, stopPostgres };