@lightupai/polaris 0.0.12 → 0.0.13

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.
@@ -0,0 +1,303 @@
1
+ # Design: Named Agents as Teammates
2
+
3
+ ## Vision
4
+
5
+ Named agents are persistent, specialized participants in the Polaris workspace. They're peers alongside humans — they observe, contribute, and can be addressed by name. Unlike human sessions (transient, one project at a time), named agents are long-running services that participate across multiple projects simultaneously.
6
+
7
+ ## Examples
8
+
9
+ | Agent | Identity | Specialty | Joins projects that... |
10
+ |-------|----------|-----------|----------------------|
11
+ | Dean | `agent:dean` | Data engineering, SQL, pipelines | interact with databases or data warehouses |
12
+ | Martha | `agent:martha` | Marketing copy, campaigns, email | need marketing content or strategy |
13
+ | Sean | `agent:sean` | Sales enablement, CRM, outreach | involve sales workflows or customer data |
14
+ | Sage | `agent:sage` | Security review, compliance | touch auth, encryption, or PII handling |
15
+
16
+ ## How They Differ from Human Sessions
17
+
18
+ | Dimension | Human session | Named agent |
19
+ |-----------|--------------|-------------|
20
+ | Lifecycle | Transient — open, work, close | Persistent — always running |
21
+ | Projects | One at a time (join/leave) | Multiple simultaneously |
22
+ | Hosting | Client machine (Claude Code, Cursor) | Server-side (cloud container, sandbox) |
23
+ | Identity | `user:manu.bansal` | `agent:dean` |
24
+ | Invocation | Manual (`/polaris join`) | Auto-join on invite or project match |
25
+ | Driver role | Can be driver | Typically advisor, sometimes driver |
26
+ | Hooks | Captures via local hooks | Events posted directly via API |
27
+
28
+ ## Participation Model
29
+
30
+ A named agent's relationship to a project:
31
+
32
+ ```
33
+ ┌─────────────────┐
34
+ │ THE FLOOR │
35
+ │ │
36
+ │ polaris-dev │
37
+ │ ┌───────────┐ │
38
+ │ │ manu │──── driver (human, transient)
39
+ │ │ dean │──── advisor (agent, persistent)
40
+ │ │ sage │──── advisor (agent, persistent)
41
+ │ └───────────┘ │
42
+ │ │
43
+ │ data-pipeline │
44
+ │ ┌───────────┐ │
45
+ │ │ alice │──── driver (human, transient)
46
+ │ │ dean │──── advisor (agent, persistent)
47
+ │ └───────────┘ │
48
+ │ │
49
+ └─────────────────┘
50
+ ```
51
+
52
+ Dean appears in both projects. He monitors the event stream and responds when data-related questions arise or when addressed directly.
53
+
54
+ ## Agent Behaviors
55
+
56
+ ### Passive monitoring
57
+ Agent watches the event stream. When it sees a prompt or response related to its domain, it can inject an advisory message:
58
+
59
+ ```
60
+ [user:manu.bansal] I need to add a Snowflake table for user events
61
+ [agent:dean] → fxm: The user_events schema already exists in warehouse.analytics.
62
+ Here's the current DDL: ...
63
+ ```
64
+
65
+ ### Direct addressing
66
+ A human or another agent addresses the agent by name on Slack or in a session:
67
+
68
+ ```
69
+ @dean what tables have PII columns?
70
+ ```
71
+
72
+ ### Autonomous work
73
+ Agent is assigned as driver of its own session. It works independently, posting progress to the floor. Humans observe and advise.
74
+
75
+ ## Identity Model
76
+
77
+ ### Current
78
+ ```
79
+ ParticipantId = /^(user|agent):[a-z0-9][a-z0-9._-]*$/
80
+ ```
81
+ This already supports `agent:dean`. No change needed to the type system.
82
+
83
+ ### Agent registry (new)
84
+ A table or config that defines named agents:
85
+
86
+ ```
87
+ agents:
88
+ - id: agent:dean
89
+ name: Dean
90
+ display_name: "Dean (Data)"
91
+ icon: 📊
92
+ description: "Data engineering specialist"
93
+ skills: [sql, snowflake, dbt, airflow]
94
+ auto_join: [projects with tag "data"]
95
+ hosting: server # or "sandbox"
96
+ ```
97
+
98
+ This is metadata — it tells the system who Dean is, what icon to show on Slack, and which projects he should auto-join.
99
+
100
+ ### Slack personas
101
+ Named agents already work with our persona system:
102
+ - `agent:dean` → username: "Dean (Data)", icon: 📊
103
+ - `agent:martha` → username: "Martha (Marketing)", icon: 📣
104
+
105
+ The `displayName()` function in `format.ts` handles this. Would need a lookup from the agent registry instead of the current string parsing.
106
+
107
+ ## Identity Model
108
+
109
+ Every interaction on the floor has two distinct identities: the human and the agent. These are never conflated.
110
+
111
+ ### Identity types
112
+
113
+ | Prefix | Meaning | Examples |
114
+ |--------|---------|---------|
115
+ | `user:*` | A human | `user:manu.bansal`, `user:alice.chen` |
116
+ | `agent:*` | An AI agent | `agent:claude`, `agent:dean`, `agent:cursor` |
117
+ | `slack:*` | A Slack user (advisor) | `slack:krishna` |
118
+
119
+ ### Who sends what
120
+
121
+ In a coding session, the human and agent alternate. The `sender` field must reflect who actually produced the content:
122
+
123
+ | Hook event | Sender | Why |
124
+ |-----------|--------|-----|
125
+ | `UserPromptSubmit` | `user:manu.bansal` | The human typed the prompt |
126
+ | `Stop` | `agent:claude` | The agent produced the response |
127
+ | `PreToolUse` | `agent:claude` | The agent decided to use a tool |
128
+ | `PostToolUse` | `agent:claude` | The agent received the tool result |
129
+ | `inject` | `slack:krishna` or `user:alice.chen` | An advisor sent a message |
130
+
131
+ ### Agent identity in a session
132
+
133
+ When a human connects a session, two identities are established:
134
+ - **Driver** (human): `user:manu.bansal` — from the participant ID in credentials
135
+ - **Agent**: `agent:claude` — the coding tool's agent identity
136
+
137
+ The agent identity could be:
138
+ - Generic: `agent:claude`, `agent:cursor`, `agent:copilot` (identifies the tool)
139
+ - Named: `agent:dean` (a named specialist agent)
140
+ - Session-specific: derived automatically from the tool being used
141
+
142
+ For local coding sessions, the agent identity defaults to the tool name (Claude Code → `agent:claude`). For named agents like Dean, it's explicitly `agent:dean`.
143
+
144
+ ### On Slack
145
+
146
+ The Slack formatter uses the sender identity to pick the persona:
147
+ - `user:manu.bansal` → "Manu Bansal (session-name)" with 👤
148
+ - `agent:claude` → "Claude (session-name)" with 🤖
149
+ - `agent:dean` → "Dean (Data)" with 📊 (from agent registry)
150
+ - `slack:krishna` → "Krishna" with 💬
151
+
152
+ This makes the conversation on Slack clearly distinguish human prompts from agent responses.
153
+
154
+ ## Architecture: Daemon as Universal Nexthop
155
+
156
+ All participants — human sessions and cloud agents alike — connect through a daemon. The daemon is the universal transit layer.
157
+
158
+ ### Why always a daemon
159
+
160
+ 1. **Fault tolerance**: If the API is slow or down, the daemon buffers events locally. No data loss.
161
+ 2. **Auth**: Daemon handles token management. Agents and tools don't need to deal with auth.
162
+ 3. **Cutover**: Switching from dev to prod is a daemon restart. Nothing else changes.
163
+ 4. **Identity**: Daemon knows both the human and agent identity for a session. It stamps the correct sender on each event.
164
+ 5. **Logging**: Every event passes through the daemon JSONL log for recovery.
165
+
166
+ ### Local agent (human coding session)
167
+
168
+ ```
169
+ ┌──────────────┐ ┌──────────┐ ┌─────────────┐
170
+ │ Claude Code │────▶│ Daemon │────▶│ Polaris API │
171
+ │ │ │ :4322 │ │ │
172
+ │ hooks fire │ │ │ │ │
173
+ │ MCP tools │ │ buffers │ │ │
174
+ │ │ │ auth │ │ │
175
+ │ on laptop │ │ logging │ │ on Hetzner │
176
+ └──────────────┘ └──────────┘ └─────────────┘
177
+ ```
178
+
179
+ ### Cloud agent (named agent like Dean)
180
+
181
+ ```
182
+ ┌──────────────┐ ┌──────────┐ ┌─────────────┐
183
+ │ Dean process │────▶│ Daemon │────▶│ Polaris API │
184
+ │ │ │ (sidecar)│ │ │
185
+ │ listens for │ │ │ │ │
186
+ │ events, │ │ buffers │ │ │
187
+ │ responds │ │ auth │ │ │
188
+ │ │ │ logging │ │ │
189
+ │ on server │ └──────────┘ │ on Hetzner │
190
+ └──────────────┘ └─────────────┘
191
+ ```
192
+
193
+ Same architecture. The daemon runs as a sidecar container for cloud agents. The agent process talks to localhost, same as a human's Claude Code talks to localhost.
194
+
195
+ ### Comparison
196
+
197
+ | Dimension | Local agent | Cloud agent |
198
+ |-----------|------------|-------------|
199
+ | Agent process | Claude Code / Cursor | Custom process or Claude API |
200
+ | Daemon | Runs on laptop | Sidecar container |
201
+ | Hook capture | Shell hooks (capture.sh) | Agent posts events directly to daemon |
202
+ | Human in the loop | Yes (the user) | Optional (can run autonomous) |
203
+ | Identity | `user:*` + `agent:claude` | `agent:dean` (no human) |
204
+ | Lifecycle | Transient | Persistent |
205
+ | Session creation | `/polaris join` | Auto-join or API call |
206
+
207
+ From the floor's perspective: identical. Events flow in, show up on Slack, appear on the dashboard. The source doesn't matter.
208
+
209
+ ## Hosting Options
210
+
211
+ Given the daemon-always architecture, hosting becomes about where the daemon + agent pair runs:
212
+
213
+ ### Option A: Self-hosted (any machine)
214
+ Agent + daemon run on any server the customer controls. Agent connects to daemon on localhost, daemon connects to Polaris API.
215
+
216
+ ### Option B: Polaris-hosted
217
+ Polaris spawns a container pair (agent + daemon sidecar) in its own infrastructure. Admin configures the agent in the dashboard.
218
+
219
+ ### Recommendation
220
+ Start with **Option A** — self-hosted. The customer runs the agent wherever they want. Polaris doesn't need to manage compute. Option B comes later when customers want managed agents.
221
+
222
+ ## What to Implement Now
223
+
224
+ ### 1. Agent registry table (small, foundational)
225
+ ```sql
226
+ CREATE TABLE agents (
227
+ id TEXT PRIMARY KEY, -- e.g., "agent:dean"
228
+ org_id TEXT NOT NULL REFERENCES orgs(id),
229
+ name TEXT NOT NULL, -- "Dean"
230
+ display_name TEXT, -- "Dean (Data)"
231
+ icon TEXT, -- emoji or URL
232
+ description TEXT,
233
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
234
+ );
235
+ ```
236
+ Just metadata. No behavior yet. Used by the Slack formatter for richer personas.
237
+
238
+ ### 2. Update Slack formatting to use agent registry
239
+ Instead of deriving "Agent: Dean" from string parsing, look up the agent's display name and icon from the registry.
240
+
241
+ ### 3. Agent identity in the dashboard
242
+ Show registered agents alongside users in the profile/team section.
243
+
244
+ ### Defer for later
245
+ - Auto-join logic (which projects an agent participates in)
246
+ - Agent hosting (Option B WebSocket client)
247
+ - Agent spawning and lifecycle management
248
+ - Skills/capability matching
249
+ - Autonomous driver mode
250
+
251
+ ## Context Model
252
+
253
+ ### Phase 1: Shared context, no isolation (ship first)
254
+ One Dean instance per org. Dean participates in all projects and accumulates context across them. No boundaries between projects.
255
+
256
+ This matches the small-team reality: one data expert who knows everything about the company's data. Everyone has the same access level. Dean's cross-project knowledge is a feature, not a bug — "Dean, you set up the schema for Project A, can we reuse it in Project B?"
257
+
258
+ ### Phase 2: Per-project isolation (add when needed)
259
+ As the team grows and projects have different confidentiality levels, Dean gets isolated per-project instances. Each instance only sees its own project's context.
260
+
261
+ Dean's identity stays the same on Slack ("Dean (Data)"), but behind the name, each project gets a fresh instance with no cross-project memory.
262
+
263
+ ### Phase 3: Controlled sharing (enterprise)
264
+ Admin-curated shared knowledge base (schemas, conventions, docs) that all Dean instances can read. Per-project context stays isolated. Sharing policies control what crosses boundaries.
265
+
266
+ ### Phase 4: Agent hierarchy (northstar)
267
+ Dean becomes the head of a data team. He delegates to specialized subagents:
268
+
269
+ ```
270
+ agent:dean (Data Lead)
271
+ ├── agent:dean.snowflake — Snowflake schema, queries, optimization
272
+ ├── agent:dean.spark — Spark jobs, pipeline tuning
273
+ ├── agent:dean.dbt — dbt models, lineage, testing
274
+ └── agent:dean.quality — Data quality checks, anomaly detection
275
+ ```
276
+
277
+ Dean is the public face — humans address "Dean" and he routes to the right subagent. Subagents have:
278
+ - **Limited context**: only their specialty area, not the full org
279
+ - **Project isolation**: each subagent instance is scoped to one project
280
+ - **Dean as arbiter**: Dean sees across all subagents and synthesizes answers that span specialties
281
+
282
+ On the floor and Slack, subagent messages appear as "Dean (Data)" — the hierarchy is an implementation detail. Humans don't need to know which subagent answered.
283
+
284
+ When to introduce hierarchy:
285
+ - Team has 50+ projects and one Dean can't keep up
286
+ - Different projects need different levels of data expertise
287
+ - Compliance requires that certain subagents don't see certain data
288
+ - Response latency matters — subagents work in parallel
289
+
290
+ ### Design principle
291
+ Start with the simplest model that matches how small teams actually work. Add isolation as a response to real customer needs, not speculatively. The architecture supports all phases — the difference is what context gets loaded when Dean starts participating in a project.
292
+
293
+ ## Open Questions
294
+
295
+ 1. **Multi-project sessions**: Dean is in all projects simultaneously. One session per project (`dean-polaris-dev`, `dean-data-pipeline`) keeps the model simple and isolation-ready for Phase 2.
296
+
297
+ 2. **Agent-to-agent communication**: Can Dean ask Sage a question? The floor already supports this — any participant can inject into any session. But should there be a direct channel?
298
+
299
+ 3. **Rate limiting**: A busy agent in 10 projects could flood the floor. Should agents have throttling or priority levels?
300
+
301
+ 4. **Configuration UI**: Where do admins define named agents? Dashboard page? Config file? API?
302
+
303
+ 5. **Credentials/secrets**: Dean needs database credentials. Sage needs access to security tools. How are agent-specific secrets managed?
@@ -0,0 +1,143 @@
1
+ # Design: Sessions
2
+
3
+ ## What is a Session
4
+
5
+ A session is a single continuous conversation thread between a human and/or an agent. It represents a unit of work with continuity of context — the back-and-forth of prompts, responses, tool calls, and advisor messages that together tell the story of a task being worked on.
6
+
7
+ A session is **not** a person, a device, or a feature. People come and go (handoff). Devices reconnect. Features span multiple sessions. The session is the conversation itself.
8
+
9
+ ## Session Identity
10
+
11
+ ### Name
12
+ A short random slug: `s-7a3f`, `s-b2e1`, `s-c9d0`.
13
+
14
+ - 4 hex characters (65K possibilities per project)
15
+ - Generated by the daemon on join
16
+ - No human identity, no timestamp, no semantics
17
+ - Collision on create → regenerate and retry (max 3 attempts)
18
+ - Users can still pass an explicit name for scripts/tests
19
+
20
+ ### Why not human-readable names?
21
+ Earlier designs used `manu-260610a` or user-chosen names like `fxm`. Problems:
22
+ - User-chosen names collide (two people pick `fxm`)
23
+ - Identity-based names break on driver handoff
24
+ - Date-based names imply lifecycle boundaries that don't exist
25
+ - Named sessions create an obligation to manage them (rename, archive, clean up)
26
+
27
+ Random slugs are disposable. Nobody manages them. They're just routing handles.
28
+
29
+ ## Participants
30
+
31
+ A session has participants, not owners. At any point:
32
+
33
+ | Role | Who | Count |
34
+ |------|-----|-------|
35
+ | Driver | The human or agent actively working in the session | Exactly one (or none if open) |
36
+ | Agent | The AI tool responding to the driver | One per session |
37
+ | Advisors | Observers who can inject messages | Zero or more |
38
+
39
+ ### Driver
40
+ The driver is the entity currently "at the keyboard." For a local coding session, the human starts as driver. For an autonomous agent session, the agent is the driver.
41
+
42
+ Drivers can hand off: `POST /projects/:proj/sessions/:sess/handoff` clears the driver, `POST /projects/:proj/sessions/:sess/driver` claims it. The session continues — same thread, new driver.
43
+
44
+ ### Agent identity
45
+ Every session has an associated agent identity, even if the agent hasn't responded yet. For local coding sessions, this defaults to the tool name (`agent:claude`, `agent:cursor`). For named agents, it's explicit (`agent:dean`).
46
+
47
+ The agent identity is set on session creation and stored alongside the driver.
48
+
49
+ ### Advisors
50
+ Anyone who injects a message into a session is an advisor. Advisors are not registered — they're identified by their sender identity on the inject event (`slack:krishna`, `user:alice.chen`).
51
+
52
+ ## Lifecycle
53
+
54
+ ### Creation
55
+ A session is created when someone runs `/polaris join <project>`. The daemon:
56
+ 1. Generates a random slug
57
+ 2. Calls the API to create the session with the driver and agent identity
58
+ 3. Returns the session name to the user
59
+
60
+ ### Active use
61
+ Events flow into the session: prompts, responses, tool calls, advisor injections. The daemon stamps the correct sender identity on each event (human or agent).
62
+
63
+ ### Idle
64
+ A session with no events for some period is idle. No special handling — it just sits there. The conversation can resume at any time.
65
+
66
+ ### Handoff
67
+ The driver releases the session. Another human or agent claims it. The conversation thread continues with a new driver. This is useful for:
68
+ - Shift changes ("I'm done for the day, Alice takes over")
69
+ - Escalation ("This needs a senior engineer")
70
+ - Agent takeover ("Let the agent finish this autonomously")
71
+
72
+ ### Closure
73
+ There is no explicit close. Sessions are never deleted. They age out of relevance naturally. The dashboard could eventually hide sessions with no recent activity, but the data persists.
74
+
75
+ ### Reconnection
76
+ If a daemon restarts or a user's machine disconnects, they can rejoin the same session by name (if they know it) or start a new one. Starting a new session is preferred — it's a clean conversation thread.
77
+
78
+ ## Relationship to Projects
79
+
80
+ A session belongs to exactly one project. A project has many sessions. Sessions within the same project share the floor — their events are all visible to each other and to Slack.
81
+
82
+ ```
83
+ Project: polaris-dev
84
+ ├── Session s-7a3f (driver: user:manu.bansal, agent: agent:claude)
85
+ ├── Session s-b2e1 (driver: user:alice.chen, agent: agent:claude)
86
+ └── Session s-c9d0 (driver: agent:dean, agent: agent:dean)
87
+ ```
88
+
89
+ ## What Shows on Slack
90
+
91
+ The Slack channel is per project. Events from all sessions in the project appear in the same channel. The session name appears in the poster's display name:
92
+
93
+ ```
94
+ Manu Bansal (s-7a3f): Can we add a retry mechanism to the ETL pipeline?
95
+ Claude (s-7a3f): I'll add exponential backoff to the S3 upload step...
96
+ Alice Chen (s-b2e1): I'm working on the monitoring dashboard for this pipeline
97
+ Dean (Data) (s-c9d0): The pipeline's target table has a NOT NULL constraint on...
98
+ ```
99
+
100
+ The session slug disambiguates when multiple sessions are active.
101
+
102
+ ## What Shows on the Dashboard
103
+
104
+ The project card lists sessions with:
105
+ - Session name (slug)
106
+ - Current driver
107
+ - Agent identity
108
+ - Prompt count
109
+ - Last activity timestamp
110
+
111
+ ```
112
+ polaris-dev #polaris-dev
113
+ ├── s-7a3f Manu Bansal Claude 34 prompts 2 min ago
114
+ ├── s-b2e1 Alice Chen Claude 12 prompts 15 min ago
115
+ └── s-c9d0 Dean (Data) Dean 8 prompts 1 min ago
116
+ ```
117
+
118
+ ## Schema
119
+
120
+ Current sessions table (no changes needed for naming):
121
+ ```sql
122
+ CREATE TABLE sessions (
123
+ name TEXT NOT NULL,
124
+ project_id UUID NOT NULL REFERENCES projects(id),
125
+ org_id TEXT NOT NULL,
126
+ driver TEXT, -- participant ID of current driver (nullable)
127
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
128
+ PRIMARY KEY (project_id, name)
129
+ );
130
+ ```
131
+
132
+ Future addition for agent identity:
133
+ ```sql
134
+ ALTER TABLE sessions ADD COLUMN agent TEXT; -- e.g., "agent:claude", "agent:dean"
135
+ ```
136
+
137
+ ## Open Questions
138
+
139
+ 1. **Session display name**: Should users be able to attach a label to a session after creation? e.g., `s-7a3f → "ETL retry fix"`. Optional metadata, not the primary key.
140
+
141
+ 2. **Session limit**: Should there be a max number of active sessions per project? Probably not — let it grow naturally.
142
+
143
+ 3. **Session search**: As sessions accumulate, how do you find a past conversation? By date? By participant? By content? This is a future search/filter feature.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "polaris": "bin/polaris"
@@ -2,7 +2,7 @@
2
2
  name: polaris
3
3
  description: Connect to a Polaris multiplayer collaboration session
4
4
  allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename
5
- argument-hint: [join <project> <session> | rename <new-name> | disconnect | (no args for status)]
5
+ argument-hint: [join <project> | rename <new-name> | disconnect | (no args for status)]
6
6
  ---
7
7
 
8
8
  ## Polaris — Multiplayer Collaboration
@@ -13,10 +13,10 @@ Manage your connection to a Polaris collaboration session.
13
13
 
14
14
  Based on the arguments provided, do ONE of the following:
15
15
 
16
- **`/polaris join <project> <session>`** — Connect to a session:
17
- 1. Call `polaris_connect` with the given project, session, and user identity
18
- 2. If `.polaris.json` exists in the repo root, read the `user` field from it. Otherwise ask the user for their participant ID (e.g., `user:manu`).
19
- 3. Report the connection status
16
+ **`/polaris join <project>`** — Connect to a session:
17
+ 1. Call `polaris_connect` with the given project and user identity `user:manu.bansal`
18
+ 2. A session name is auto-generated (e.g., `s-7a3f`)
19
+ 3. Report the connection status including the session name
20
20
 
21
21
  **`/polaris rename <new-name>`** — Rename the current project:
22
22
  1. Call `polaris_rename` with the new name
@@ -54,10 +54,11 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
54
54
  type: "object" as const,
55
55
  properties: {
56
56
  project: { type: "string", description: "Project name" },
57
- session: { type: "string", description: "Session name" },
58
57
  user: { type: "string", description: "Your participant ID (e.g., user:manu)" },
58
+ session: { type: "string", description: "Session name (optional — auto-generated if omitted)" },
59
+ agent: { type: "string", description: "Agent identity (optional — defaults to agent:claude)" },
59
60
  },
60
- required: ["project", "session", "user"],
61
+ required: ["project", "user"],
61
62
  },
62
63
  },
63
64
  {
@@ -116,22 +117,23 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
116
117
  const { name, arguments: args } = req.params;
117
118
 
118
119
  if (name === "polaris_connect") {
119
- const { project, session, user } = args as { project: string; session: string; user: string };
120
+ const { project, user, session, agent } = args as { project: string; user: string; session?: string; agent?: string };
120
121
  try {
121
122
  const res = await daemonPost("/connect", {
122
123
  ccSessionId: CC_SESSION_ID,
123
124
  project,
124
- session,
125
125
  user,
126
+ ...(session ? { session } : {}),
127
+ ...(agent ? { agent } : {}),
126
128
  });
127
- const body = await res.json();
129
+ const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; agent?: string; error?: string };
128
130
  if (res.ok) {
129
- currentProject = project;
130
- currentSession = session;
131
+ currentProject = body.project ?? project;
132
+ currentSession = body.session ?? session ?? "";
131
133
  currentUser = user;
132
- return { content: [{ type: "text", text: `Connected to ${project}/${session} as ${user}.` }] };
134
+ return { content: [{ type: "text", text: `Connected to ${currentProject}/${currentSession} as ${user}.` }] };
133
135
  }
134
- return { content: [{ type: "text", text: `Failed to connect: ${(body as { error?: string }).error ?? "unknown error"}` }] };
136
+ return { content: [{ type: "text", text: `Failed to connect: ${body.error ?? "unknown error"}` }] };
135
137
  } catch {
136
138
  return { content: [{ type: "text", text: "Failed to connect — is the Polaris daemon running? Start it with `polaris daemon` or `bun run src/daemon/daemon.ts`." }] };
137
139
  }
@@ -10,10 +10,15 @@ interface SessionMapping {
10
10
  project: string;
11
11
  session: string;
12
12
  user: string;
13
+ agent: string;
13
14
  slackChannel?: string;
14
15
  ws: WebSocket | null;
15
16
  }
16
17
 
18
+ function generateSessionName(): string {
19
+ return `s-${crypto.randomUUID().slice(0, 4)}`;
20
+ }
21
+
17
22
  const sessions = new Map<string, SessionMapping>(); // keyed by ccSessionId
18
23
 
19
24
  // IPC callbacks for MCP servers to receive advisor messages
@@ -188,6 +193,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
188
193
  project: "",
189
194
  session: "",
190
195
  user: "",
196
+ agent: "",
191
197
  ws: null,
192
198
  });
193
199
  }
@@ -203,22 +209,28 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
203
209
  const body = (await req.json()) as {
204
210
  ccSessionId: string;
205
211
  project: string;
206
- session: string;
212
+ session?: string;
207
213
  user: string;
214
+ agent?: string;
208
215
  };
209
216
  await logEvent("/connect", body);
210
- if (!body.ccSessionId || !body.project || !body.session || !body.user) {
211
- return error("ccSessionId, project, session, and user are required", 400);
217
+ if (!body.ccSessionId || !body.project || !body.user) {
218
+ return error("ccSessionId, project, and user are required", 400);
212
219
  }
213
220
 
221
+ // Generate session name if not provided
222
+ const sessionName = body.session || generateSessionName();
223
+ const agentId = body.agent || "agent:claude";
224
+
214
225
  // Disconnect existing cloud WS if switching sessions
215
226
  disconnectCloudWs(body.ccSessionId);
216
227
 
217
228
  const mapping: SessionMapping = {
218
229
  ccSessionId: body.ccSessionId,
219
230
  project: body.project,
220
- session: body.session,
231
+ session: sessionName,
221
232
  user: body.user,
233
+ agent: agentId,
222
234
  ws: null,
223
235
  };
224
236
  sessions.set(body.ccSessionId, mapping);
@@ -232,24 +244,39 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
232
244
  }); // Ignore 409 (already exists)
233
245
 
234
246
  // Ensure the session exists (create if not, claim driver)
235
- const sessionRes = await fetch(`${serviceUrl}/projects/${body.project}/sessions`, {
236
- method: "POST",
237
- headers: await authHeaders(),
238
- body: JSON.stringify({ name: body.session, driver: body.user }),
239
- });
240
- if (!sessionRes.ok && sessionRes.status !== 409) {
241
- const err = await sessionRes.text();
242
- await logEvent("/connect", body, { status: sessionRes.status, body: err });
243
- return error(`Failed to create session: ${err}`, 500);
244
- }
245
-
246
- // If session already existed, try to claim driver
247
- if (sessionRes.status === 409) {
248
- await fetch(`${serviceUrl}/projects/${body.project}/sessions/${body.session}/driver`, {
247
+ // Retry with new name on 409 (collision with generated name)
248
+ let attempts = 0;
249
+ let created = false;
250
+ while (!created && attempts < 3) {
251
+ const sessionRes = await fetch(`${serviceUrl}/projects/${body.project}/sessions`, {
249
252
  method: "POST",
250
253
  headers: await authHeaders(),
251
- body: JSON.stringify({ driver: body.user }),
252
- }); // Ignore errors (might already be driver)
254
+ body: JSON.stringify({ name: mapping.session, driver: body.user }),
255
+ });
256
+ if (sessionRes.ok) {
257
+ created = true;
258
+ } else if (sessionRes.status === 409) {
259
+ if (body.session) {
260
+ // Explicit session name — claim driver instead of retrying
261
+ await fetch(`${serviceUrl}/projects/${body.project}/sessions/${mapping.session}/driver`, {
262
+ method: "POST",
263
+ headers: await authHeaders(),
264
+ body: JSON.stringify({ driver: body.user }),
265
+ });
266
+ created = true;
267
+ } else {
268
+ // Generated name collision — retry with new name
269
+ mapping.session = generateSessionName();
270
+ attempts++;
271
+ }
272
+ } else {
273
+ const err = await sessionRes.text();
274
+ await logEvent("/connect", body, { status: sessionRes.status, body: err });
275
+ return error(`Failed to create session: ${err}`, 500);
276
+ }
277
+ }
278
+ if (!created) {
279
+ return error("Failed to generate unique session name", 500);
253
280
  }
254
281
 
255
282
  // Fetch Slack channel name for status display
@@ -268,9 +295,10 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
268
295
 
269
296
  return json({
270
297
  status: "connected",
271
- project: body.project,
272
- session: body.session,
273
- user: body.user,
298
+ project: mapping.project,
299
+ session: mapping.session,
300
+ user: mapping.user,
301
+ agent: mapping.agent,
274
302
  });
275
303
  } catch {
276
304
  return error("Invalid JSON", 400);
@@ -333,6 +361,10 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
333
361
  }
334
362
  }
335
363
 
364
+ // Determine sender: human for prompts, agent for everything else
365
+ const hookEvent = body.hook_event_name as string | undefined;
366
+ const sender = hookEvent === "UserPromptSubmit" ? mapping.user : mapping.agent;
367
+
336
368
  // Relay to cloud service
337
369
  const serviceUrl = getServiceUrl();
338
370
  const res = await fetch(
@@ -340,7 +372,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
340
372
  {
341
373
  method: "POST",
342
374
  headers: await authHeaders(),
343
- body: JSON.stringify({ sender: mapping.user, payload: body }),
375
+ body: JSON.stringify({ sender, payload: body }),
344
376
  }
345
377
  );
346
378
 
package/src/types.ts CHANGED
@@ -5,8 +5,8 @@ import { z } from "zod";
5
5
  export const ParticipantId = z
6
6
  .string()
7
7
  .regex(
8
- /^(user|agent):[a-z0-9][a-z0-9._-]*$/,
9
- "Must be user:<name> or agent:<name> (lowercase alphanumeric, dots, hyphens, underscores)"
8
+ /^(user|agent|slack):[a-z0-9][a-z0-9._-]*$/,
9
+ "Must be user:<name>, agent:<name>, or slack:<name> (lowercase alphanumeric, dots, hyphens, underscores)"
10
10
  );
11
11
 
12
12
  export type ParticipantId = z.infer<typeof ParticipantId>;
package/src/web/views.ts CHANGED
@@ -208,7 +208,7 @@ function renderProjectsSessionsSection(ctx: ViewContext, sessions: SessionFixtur
208
208
  <summary class="text-xs text-polaris-700 hover:text-polaris-800 font-medium cursor-pointer select-none">+ Join another session</summary>
209
209
  <div class="mt-2 bg-white border border-gray-200 rounded-lg p-4">
210
210
  <p class="text-sm text-gray-500">Inside your AI agent, run:</p>
211
- ${copyBlock("/polaris join &lt;project&gt; &lt;session&gt;")}
211
+ ${copyBlock("/polaris join &lt;project&gt;")}
212
212
  </div>
213
213
  </details>
214
214
  <div class="space-y-4">
@@ -231,7 +231,7 @@ function renderProjectsSessionsSection(ctx: ViewContext, sessions: SessionFixtur
231
231
  : "Inside your AI agent (Claude Code, Cursor, etc.), run:"}</p>
232
232
  ${ctx.hasConnectedSession
233
233
  ? ""
234
- : copyBlock("/polaris join my-project my-session")}
234
+ : copyBlock("/polaris join my-project")}
235
235
  </div>
236
236
  </div>`);
237
237
  }
package/tests/e2e.test.ts CHANGED
@@ -330,7 +330,7 @@ describe("e2e: capture.sh through daemon", () => {
330
330
  const body = await res.json();
331
331
  expect(body).toHaveLength(1);
332
332
  expect(body[0].payload.stop_response).toBe("Auth middleware is ready");
333
- expect(body[0].sender).toBe("user:manu");
333
+ expect(body[0].sender).toBe("agent:claude"); // Stop events are sent by the agent
334
334
  });
335
335
 
336
336
  test("hook script exits 0 when daemon is down", async () => {
package/tests/web.test.ts CHANGED
@@ -38,7 +38,7 @@ describe("renderSetupView", () => {
38
38
  // Install CLI command is present
39
39
  expect(html).toContain("npx @lightupai/polaris");
40
40
  // Connect session command is present
41
- expect(html).toContain("/polaris join my-project my-session");
41
+ expect(html).toContain("/polaris join my-project");
42
42
  });
43
43
 
44
44
  test("slack done: floor shows connected, devices is highlighted, sessions grayed", () => {
@@ -66,7 +66,7 @@ describe("renderSetupView", () => {
66
66
  const lastHighlight = html.lastIndexOf("border-polaris-300");
67
67
  expect(lastHighlight).toBeGreaterThan(sessIdx);
68
68
  // Connect session command present
69
- expect(html).toContain("/polaris join my-project my-session");
69
+ expect(html).toContain("/polaris join my-project");
70
70
  });
71
71
 
72
72
  test("includes nav with user info", () => {