tycono 0.1.73 → 0.1.74-beta.0
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.
- package/README.md +129 -48
- package/package.json +1 -1
- package/src/api/src/routes/execute.ts +17 -2
- package/src/api/src/routes/operations.ts +4 -1
- package/src/api/src/routes/sessions.ts +42 -14
- package/src/api/src/services/activity-stream.ts +5 -0
- package/src/api/src/services/job-manager.ts +72 -2
- package/src/api/src/services/wave-tracker.ts +217 -0
- package/src/web/dist/assets/index-0e6kn9Ne.js +108 -0
- package/src/web/dist/assets/index-BLBYHBP_.css +1 -0
- package/src/web/dist/assets/{preview-app-BT_tZB55.js → preview-app-B3kFNGV1.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-BQaKrdJG.css +0 -1
- package/src/web/dist/assets/index-DVuy5FDy.js +0 -108
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src=".github/assets/hero-office.png" alt="Tycono — AI Office" width="
|
|
2
|
+
<img src=".github/assets/hero-office.png" alt="Tycono — AI Office" width="720" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">tycono</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>Build an AI company. Watch them work.</strong>
|
|
8
|
+
<strong>Build an AI company. Watch them work.</strong><br>
|
|
9
|
+
<sub>Infrastructure-as-Code defined servers. Company-as-Code defines organizations.</sub>
|
|
9
10
|
</p>
|
|
10
11
|
|
|
11
12
|
<p align="center">
|
|
@@ -15,94 +16,168 @@
|
|
|
15
16
|
</p>
|
|
16
17
|
|
|
17
18
|
<p align="center">
|
|
18
|
-
<a href="https://tycono.ai">Website</a>
|
|
19
|
-
<a href="#quick-start">Quick Start</a>
|
|
20
|
-
<a href="#how-it-works">How It Works</a>
|
|
19
|
+
<a href="https://tycono.ai">Website</a> ·
|
|
20
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
21
|
+
<a href="#how-it-works">How It Works</a> ·
|
|
22
|
+
<a href="#company-as-code">Company-as-Code</a> ·
|
|
21
23
|
<a href="CONTRIBUTING.md">Contributing</a>
|
|
22
24
|
</p>
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
|
26
|
-
**tycono** is an open-source platform that lets you
|
|
28
|
+
**tycono** is an open-source platform that lets you define and run an AI-powered organization. Roles, authority, knowledge, and workflows — all defined in files, executed by AI agents, visualized in real time.
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
One command. Your AI company is running.
|
|
29
31
|
|
|
30
32
|
```bash
|
|
31
|
-
mkdir my-company && cd my-company
|
|
32
33
|
npx tycono
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
That's it. A setup wizard guides you through creating your company, then your browser opens to a live dashboard showing your AI team at work.
|
|
36
|
-
|
|
37
36
|
## Why Tycono?
|
|
38
37
|
|
|
38
|
+
Coding agents simulate **one developer**. Tycono simulates **the entire company**.
|
|
39
|
+
|
|
39
40
|
| | Single AI Agent | Tycono |
|
|
40
41
|
|---|---|---|
|
|
41
|
-
| **
|
|
42
|
-
| **Knowledge** |
|
|
43
|
-
| **Authority** | Can do anything | Scoped — each role has boundaries |
|
|
44
|
-
| **Delegation** | Manual prompt chaining |
|
|
45
|
-
| **
|
|
42
|
+
| **What it runs** | One agent, one context | Multiple roles with org hierarchy |
|
|
43
|
+
| **Knowledge** | Resets every session | Compounds forever — file-based, cross-linked |
|
|
44
|
+
| **Authority** | Can do anything (or nothing) | Scoped — each role has clear boundaries |
|
|
45
|
+
| **Delegation** | Manual prompt chaining | CEO dispatches, org chart routes automatically |
|
|
46
|
+
| **Scale** | 1 agent | 7 → 700 agents |
|
|
47
|
+
| **Visibility** | Terminal output | Isometric office + Slack-style Pro dashboard |
|
|
48
|
+
|
|
49
|
+
## Company-as-Code
|
|
50
|
+
|
|
51
|
+
Just as Terraform turns `.tf` files into running infrastructure, Tycono turns YAML and Markdown into a running company.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
IaC CaC (Company-as-Code)
|
|
55
|
+
───────────────────── ─────────────────────
|
|
56
|
+
.tf → servers role.yaml → org structure
|
|
57
|
+
playbook → config CLAUDE.md → operating rules
|
|
58
|
+
Dockerfile → containers skills/ → capabilities
|
|
59
|
+
state file → infra state knowledge/ → org memory
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Your company is **versionable**, **reproducible**, and **forkable** — just like code.
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
mkdir my-company && cd my-company
|
|
68
|
+
npx tycono
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A setup wizard guides you through:
|
|
72
|
+
|
|
73
|
+
1. **Pick an AI engine** — Claude API, Claude Max, or auto-detect
|
|
74
|
+
2. **Name your company** — set mission and domain
|
|
75
|
+
3. **Choose a team template** — or build from scratch
|
|
76
|
+
4. **Watch them work** — your browser opens to a live dashboard
|
|
77
|
+
|
|
78
|
+
### Requirements
|
|
79
|
+
|
|
80
|
+
- Node.js >= 18
|
|
81
|
+
- [Anthropic API key](https://console.anthropic.com/) or Claude Max subscription
|
|
82
|
+
|
|
83
|
+
## Two Ways to Work
|
|
46
84
|
|
|
47
|
-
|
|
85
|
+
### Office View — Watch your AI team
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
- **Org hierarchy** — Roles report to each other. CTO dispatches to Engineers. PM coordinates with Design.
|
|
51
|
-
- **Real-time dashboard** — Watch your AI team work in an isometric pixel-art office
|
|
52
|
-
- **Knowledge management** — Automatic document routing, cross-linking, and Hub-based organization
|
|
53
|
-
- **Local-first** — Everything runs on your machine. Your data stays yours.
|
|
54
|
-
- **BYOK** — Bring your own Anthropic API key. No middleman.
|
|
87
|
+
An isometric pixel-art office where your AI agents sit at their desks, work, chat, and think. Click any agent to talk to them directly.
|
|
55
88
|
|
|
56
89
|
<p align="center">
|
|
57
|
-
<img src=".github/assets/
|
|
90
|
+
<img src=".github/assets/hero-office.png" alt="Office View" width="640" />
|
|
58
91
|
</p>
|
|
59
92
|
|
|
60
|
-
|
|
93
|
+
- Pixel-art characters with personalities and levels
|
|
94
|
+
- Ambient speech bubbles — agents think out loud
|
|
95
|
+
- Rooms: Leadership, Engineering, Meeting, Knowledge Library
|
|
96
|
+
- Edit mode — rearrange furniture, customize your office
|
|
61
97
|
|
|
62
|
-
|
|
63
|
-
- [Anthropic API key](https://console.anthropic.com/)
|
|
98
|
+
### Pro View — Manage your AI company
|
|
64
99
|
|
|
65
|
-
|
|
100
|
+
A Slack-style professional dashboard for serious work. Chats, Wave dispatch, Decisions log, Knowledge graph.
|
|
66
101
|
|
|
67
|
-
|
|
102
|
+
<p align="center">
|
|
103
|
+
<img src=".github/assets/pro-view.png" alt="Pro View" width="640" />
|
|
104
|
+
</p>
|
|
68
105
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
- **Wave Center** — selective org-tree dispatch with target checkboxes
|
|
107
|
+
- **Chats** — 1:1 conversations with any role, persistent sessions
|
|
108
|
+
- **Knowledge Base** — graph/tree/list views, 194+ cross-linked documents
|
|
109
|
+
- **Decisions** — CEO strategic decision log with full context
|
|
110
|
+
|
|
111
|
+
## Key Features
|
|
112
|
+
|
|
113
|
+
### CEO Wave — One order moves the company
|
|
114
|
+
|
|
115
|
+
Write a directive. Select target roles on the org tree. Hit dispatch. Every selected agent receives their piece of the work, filtered through the hierarchy.
|
|
116
|
+
|
|
117
|
+
<p align="center">
|
|
118
|
+
<img src=".github/assets/wave-center.png" alt="Wave Center — selective org-tree dispatch" width="640" />
|
|
119
|
+
</p>
|
|
120
|
+
|
|
121
|
+
### Living Knowledge (AKB)
|
|
122
|
+
|
|
123
|
+
Every task produces knowledge. Cross-linked Markdown documents that grow with every session. Search, navigate, never lose context. Session 50 is dramatically smarter than session 1.
|
|
124
|
+
|
|
125
|
+
<p align="center">
|
|
126
|
+
<img src=".github/assets/knowledge-graph.png" alt="Knowledge Base — graph view with 194+ cross-linked documents" width="640" />
|
|
127
|
+
</p>
|
|
128
|
+
|
|
129
|
+
### Role-Based Authority
|
|
130
|
+
|
|
131
|
+
Each role has scoped authority defined in `role.yaml`. Engineers can't make CEO decisions. PMs can't merge code. The org chart isn't decoration — it's enforcement.
|
|
132
|
+
|
|
133
|
+
### Level System
|
|
134
|
+
|
|
135
|
+
Roles gain XP from completed work. Level up unlocks accessories and reflects experience. Your CTO at Lv.14 has seen things your new intern hasn't.
|
|
136
|
+
|
|
137
|
+
### Local-First, BYOK
|
|
138
|
+
|
|
139
|
+
Everything runs on your machine. Your data never leaves. Bring your own Anthropic API key — no middleman, no telemetry, no tracking.
|
|
75
140
|
|
|
76
141
|
## How It Works
|
|
77
142
|
|
|
78
143
|
```
|
|
79
144
|
You (CEO)
|
|
80
|
-
└── Give
|
|
145
|
+
└── Give a directive via Wave or direct chat
|
|
81
146
|
└── Context Engine routes to the right Role
|
|
82
|
-
└── Role reads its knowledge, executes
|
|
83
|
-
└──
|
|
147
|
+
└── Role reads its knowledge + skills, executes within authority
|
|
148
|
+
└── Knowledge updates, results flow back up
|
|
149
|
+
└── Your company gets smarter
|
|
84
150
|
```
|
|
85
151
|
|
|
86
152
|
Every role has:
|
|
87
|
-
- `role.yaml` — Identity, authority, knowledge scope
|
|
88
|
-
- `SKILL.md` — Tools and
|
|
89
|
-
- `profile.md` — Public-facing description
|
|
90
|
-
- `journal/` — Work history
|
|
153
|
+
- `role.yaml` — Identity, authority, knowledge scope, reporting structure
|
|
154
|
+
- `SKILL.md` — Tools, commands, and capability guides
|
|
155
|
+
- `profile.md` — Public-facing description and persona
|
|
156
|
+
- `journal/` — Work history and learnings
|
|
91
157
|
|
|
92
158
|
## Your Company Structure
|
|
93
159
|
|
|
94
160
|
```
|
|
95
161
|
your-company/
|
|
96
|
-
├── CLAUDE.md ← AI
|
|
162
|
+
├── CLAUDE.md ← AI operating rules (auto-managed)
|
|
97
163
|
├── company/ ← Mission, vision, values
|
|
98
|
-
├── roles/ ← AI role definitions
|
|
99
|
-
├── projects/ ← Product specs and tasks
|
|
100
|
-
├── architecture/ ← Technical decisions
|
|
101
|
-
├── operations/ ← Standups, decisions,
|
|
102
|
-
├── knowledge/ ← Domain knowledge
|
|
164
|
+
├── roles/ ← AI role definitions (role.yaml + skills)
|
|
165
|
+
├── projects/ ← Product specs, PRDs, and tasks
|
|
166
|
+
├── architecture/ ← Technical decisions and designs
|
|
167
|
+
├── operations/ ← Standups, decisions, wave history
|
|
168
|
+
├── knowledge/ ← Domain knowledge (compounds over time)
|
|
103
169
|
└── .tycono/ ← Config and preferences
|
|
104
170
|
```
|
|
105
171
|
|
|
172
|
+
## Team Templates
|
|
173
|
+
|
|
174
|
+
| Template | Roles | Best For |
|
|
175
|
+
|----------|-------|----------|
|
|
176
|
+
| **Startup** | CTO + PM + Engineer + Designer | Product development |
|
|
177
|
+
| **Research** | Lead Researcher + Analyst + Writer | Analysis & reports |
|
|
178
|
+
| **Agency** | Creative Director + Designer + Developer | Client projects |
|
|
179
|
+
| **Custom** | Start empty, hire as you go | Full control |
|
|
180
|
+
|
|
106
181
|
## CLI Usage
|
|
107
182
|
|
|
108
183
|
```bash
|
|
@@ -119,6 +194,12 @@ npx tycono --version # Show version
|
|
|
119
194
|
| `PORT` | Server port | auto-detect |
|
|
120
195
|
| `COMPANY_ROOT` | Company directory | current directory |
|
|
121
196
|
|
|
197
|
+
## Built with Tycono
|
|
198
|
+
|
|
199
|
+
This isn't a demo. Tycono's own landing page, documentation, and knowledge base were built by AI agents running inside Tycono. The PM wrote the PRD. The CTO reviewed architecture. The Designer created UX specs. The Engineer implemented every section.
|
|
200
|
+
|
|
201
|
+
194 knowledge documents. 12 CEO decisions. 8 active roles. All managed through the same system you're about to use.
|
|
202
|
+
|
|
122
203
|
## Development
|
|
123
204
|
|
|
124
205
|
```bash
|
|
@@ -151,5 +232,5 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
|
151
232
|
---
|
|
152
233
|
|
|
153
234
|
<p align="center">
|
|
154
|
-
<sub>Built with
|
|
235
|
+
<sub>Built with Tycono. An AI company that builds itself.</sub>
|
|
155
236
|
</p>
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { jobManager, type Job } from '../services/job-manager.js';
|
|
17
17
|
import { ActivityStream, type ActivityEvent, type ActivitySubscriber } from '../services/activity-stream.js';
|
|
18
18
|
import { earnCoinsInternal } from './coins.js';
|
|
19
|
+
import { appendFollowUpToWave } from '../services/wave-tracker.js';
|
|
19
20
|
|
|
20
21
|
/* ─── Runner — lazy, re-created when engine changes ── */
|
|
21
22
|
|
|
@@ -103,6 +104,8 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
103
104
|
const readOnly = body.readOnly === true;
|
|
104
105
|
const targetRole = (body.targetRole as string) || 'cto';
|
|
105
106
|
const parentJobId = body.parentJobId as string | undefined;
|
|
107
|
+
const waveId = body.waveId as string | undefined;
|
|
108
|
+
const attachments = body.attachments as ImageAttachment[] | undefined;
|
|
106
109
|
|
|
107
110
|
// Wave shorthand — broadcast to C-level direct reports (optionally filtered)
|
|
108
111
|
if (type === 'wave') {
|
|
@@ -152,6 +155,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
152
155
|
type: 'directive',
|
|
153
156
|
status: 'done',
|
|
154
157
|
timestamp: new Date().toISOString(),
|
|
158
|
+
attachments,
|
|
155
159
|
};
|
|
156
160
|
addMessage(session.id, ceoMsg);
|
|
157
161
|
|
|
@@ -163,6 +167,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
163
167
|
parentJobId,
|
|
164
168
|
targetRoles: fullTargetScope,
|
|
165
169
|
sessionId: session.id, // D-014: link job to session
|
|
170
|
+
attachments,
|
|
166
171
|
});
|
|
167
172
|
jobIds.push(job.id);
|
|
168
173
|
|
|
@@ -200,7 +205,8 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
200
205
|
if (sourceRole === 'ceo' && !parentJobId) {
|
|
201
206
|
const session = createSession(roleId, {
|
|
202
207
|
mode: readOnly ? 'talk' : 'do',
|
|
203
|
-
source: 'dispatch',
|
|
208
|
+
source: waveId ? 'wave' : 'dispatch',
|
|
209
|
+
...(waveId && { waveId }),
|
|
204
210
|
});
|
|
205
211
|
sessionId = session.id;
|
|
206
212
|
|
|
@@ -212,6 +218,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
212
218
|
type: readOnly ? 'conversation' : 'directive',
|
|
213
219
|
status: 'done',
|
|
214
220
|
timestamp: new Date().toISOString(),
|
|
221
|
+
attachments,
|
|
215
222
|
};
|
|
216
223
|
addMessage(session.id, ceoMsg);
|
|
217
224
|
}
|
|
@@ -224,6 +231,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
224
231
|
readOnly,
|
|
225
232
|
parentJobId,
|
|
226
233
|
sessionId,
|
|
234
|
+
attachments,
|
|
227
235
|
});
|
|
228
236
|
|
|
229
237
|
// D-014: Add role message linked to job
|
|
@@ -241,9 +249,16 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
241
249
|
addMessage(sessionId, roleMsg, true);
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
|
|
252
|
+
// Follow-up: append this job to the wave JSON so it persists across navigation
|
|
253
|
+
if (waveId) {
|
|
254
|
+
appendFollowUpToWave(waveId, job.id, roleId, task, sessionId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
jsonResponse(res, 200, { jobId: job.id, ...(sessionId && { sessionId }), ...(waveId && { waveId }) });
|
|
245
258
|
}
|
|
246
259
|
|
|
260
|
+
/* ─── Follow-up: wave tracking (delegated to wave-tracker service) ── */
|
|
261
|
+
|
|
247
262
|
/* ─── POST /api/waves/save ──────────────── */
|
|
248
263
|
|
|
249
264
|
function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): void {
|
|
@@ -49,13 +49,16 @@ operationsRouter.get('/waves', (_req: Request, res: Response, next: NextFunction
|
|
|
49
49
|
const id = path.basename(f, '.json');
|
|
50
50
|
try {
|
|
51
51
|
const data = JSON.parse(readFile(`operations/waves/${f}`));
|
|
52
|
+
const roles = data.roles ?? [];
|
|
53
|
+
const hasRunning = roles.some((r: { status?: string }) => r.status === 'running' || r.status === 'awaiting_input');
|
|
52
54
|
return {
|
|
53
55
|
id,
|
|
54
56
|
timestamp: id,
|
|
55
57
|
directive: data.directive ?? '',
|
|
56
|
-
rolesCount:
|
|
58
|
+
rolesCount: roles.length,
|
|
57
59
|
startedAt: data.startedAt ?? '',
|
|
58
60
|
...(data.commit ? { commit: data.commit } : {}),
|
|
61
|
+
...(hasRunning ? { hasRunning: true } : {}),
|
|
59
62
|
};
|
|
60
63
|
} catch {
|
|
61
64
|
return { id, timestamp: id, directive: '', rolesCount: 0, startedAt: '' };
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../services/session-store.js';
|
|
13
13
|
import { jobManager } from '../services/job-manager.js';
|
|
14
14
|
import { ActivityStream, type ActivityEvent } from '../services/activity-stream.js';
|
|
15
|
+
import { updateFollowUpForReply } from '../services/wave-tracker.js';
|
|
15
16
|
|
|
16
17
|
export const sessionsRouter = Router();
|
|
17
18
|
|
|
@@ -187,15 +188,9 @@ sessionsRouter.post('/:id/reply', (req, res) => {
|
|
|
187
188
|
return;
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
const { message, responderRole } = req.body;
|
|
191
|
-
if (!message) {
|
|
192
|
-
res.status(400).json({ error: 'message
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const job = jobManager.getJobBySessionId(req.params.id);
|
|
197
|
-
if (!job) {
|
|
198
|
-
res.status(404).json({ error: 'No active job for this session' });
|
|
191
|
+
const { message, responderRole, attachments } = req.body;
|
|
192
|
+
if (!message && (!attachments || attachments.length === 0)) {
|
|
193
|
+
res.status(400).json({ error: 'message or attachments required' });
|
|
199
194
|
return;
|
|
200
195
|
}
|
|
201
196
|
|
|
@@ -203,17 +198,44 @@ sessionsRouter.post('/:id/reply', (req, res) => {
|
|
|
203
198
|
const ceoMsg: Message = {
|
|
204
199
|
id: `msg-${Date.now()}-ceo-reply`,
|
|
205
200
|
from: 'ceo',
|
|
206
|
-
content: message,
|
|
201
|
+
content: message ?? '',
|
|
207
202
|
type: 'conversation',
|
|
208
203
|
status: 'done',
|
|
209
204
|
timestamp: new Date().toISOString(),
|
|
205
|
+
attachments,
|
|
210
206
|
};
|
|
211
207
|
addMessage(req.params.id, ceoMsg);
|
|
212
208
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
209
|
+
const job = jobManager.getJobBySessionId(req.params.id);
|
|
210
|
+
let newJob;
|
|
211
|
+
|
|
212
|
+
if (job) {
|
|
213
|
+
// Normal path: reply to existing job
|
|
214
|
+
newJob = jobManager.replyToJob(job.id, message ?? '(image attached)', responderRole);
|
|
215
|
+
if (!newJob) {
|
|
216
|
+
res.status(400).json({ error: 'Job not in a replyable state' });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// Fallback: job lost (server restart) — create fresh follow-up job
|
|
221
|
+
// Build context from session history
|
|
222
|
+
const prevMessages = session.messages
|
|
223
|
+
.filter(m => m.id !== ceoMsg.id)
|
|
224
|
+
.slice(-6)
|
|
225
|
+
.map(m => `${m.from === 'ceo' ? 'CEO' : m.from.toUpperCase()}: ${m.content.slice(0, 500)}`)
|
|
226
|
+
.join('\n');
|
|
227
|
+
const task = prevMessages
|
|
228
|
+
? `[Conversation History]\n${prevMessages}\n\n[CEO Follow-up]\n${message ?? '(image attached)'}`
|
|
229
|
+
: (message ?? '(image attached)');
|
|
230
|
+
|
|
231
|
+
newJob = jobManager.startJob({
|
|
232
|
+
type: 'assign',
|
|
233
|
+
roleId: session.roleId,
|
|
234
|
+
task,
|
|
235
|
+
sourceRole: responderRole ?? 'ceo',
|
|
236
|
+
sessionId: req.params.id,
|
|
237
|
+
attachments,
|
|
238
|
+
});
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
// Add role message for the continuation job
|
|
@@ -228,5 +250,11 @@ sessionsRouter.post('/:id/reply', (req, res) => {
|
|
|
228
250
|
};
|
|
229
251
|
addMessage(req.params.id, roleMsg, true);
|
|
230
252
|
|
|
253
|
+
// Update wave JSON if this session belongs to a wave
|
|
254
|
+
if (session.waveId) {
|
|
255
|
+
const oldJobId = job?.id;
|
|
256
|
+
updateFollowUpForReply(session.waveId, session.roleId, oldJobId, newJob.id, req.params.id);
|
|
257
|
+
}
|
|
258
|
+
|
|
231
259
|
res.json({ ok: true, jobId: newJob.id, sessionId: req.params.id });
|
|
232
260
|
});
|
|
@@ -10,7 +10,7 @@ import { estimateCost } from './pricing.js';
|
|
|
10
10
|
import { readConfig, getConversationLimits } from './company-config.js';
|
|
11
11
|
import { postKnowledgingCheck, type KnowledgeDebtItem } from '../engine/knowledge-gate.js';
|
|
12
12
|
import { earnCoinsInternal } from '../routes/coins.js';
|
|
13
|
-
import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, appendMessageEvent, type Message } from './session-store.js';
|
|
13
|
+
import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, appendMessageEvent, type Message, type ImageAttachment } from './session-store.js';
|
|
14
14
|
import { portRegistry, type PortAllocation } from './port-registry.js';
|
|
15
15
|
|
|
16
16
|
/* ─── Types ──────────────────────────────── */
|
|
@@ -72,6 +72,8 @@ export interface StartJobParams {
|
|
|
72
72
|
targetRoles?: string[];
|
|
73
73
|
/** D-014: Link this job to a session (internal tracking) */
|
|
74
74
|
sessionId?: string;
|
|
75
|
+
/** Image attachments (base64 encoded) */
|
|
76
|
+
attachments?: ImageAttachment[];
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/* ─── Helpers ────────────────────────────── */
|
|
@@ -208,6 +210,7 @@ class JobManager {
|
|
|
208
210
|
type: params.type,
|
|
209
211
|
task: params.task,
|
|
210
212
|
sourceRole: params.sourceRole ?? 'ceo',
|
|
213
|
+
...(params.sessionId && { sessionId: params.sessionId }),
|
|
211
214
|
});
|
|
212
215
|
|
|
213
216
|
// If this job has a parent, emit dispatch:start on the parent's stream
|
|
@@ -267,6 +270,7 @@ class JobManager {
|
|
|
267
270
|
teamStatus,
|
|
268
271
|
targetRoles: params.targetRoles,
|
|
269
272
|
codeRoot: config.codeRoot,
|
|
273
|
+
attachments: params.attachments,
|
|
270
274
|
env: {
|
|
271
275
|
...process.env,
|
|
272
276
|
...portEnv,
|
|
@@ -798,7 +802,73 @@ class JobManager {
|
|
|
798
802
|
}
|
|
799
803
|
}
|
|
800
804
|
}
|
|
801
|
-
return active ?? latest;
|
|
805
|
+
if (active ?? latest) return active ?? latest;
|
|
806
|
+
|
|
807
|
+
// Fallback: scan activity stream files for historical jobs with this sessionId
|
|
808
|
+
return this.recoverJobFromStreams(sessionId);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Recover a minimal Job object from activity stream files (after server restart) */
|
|
812
|
+
private recoverJobFromStreams(sessionId: string): Job | undefined {
|
|
813
|
+
try {
|
|
814
|
+
const jobIds = ActivityStream.listAll();
|
|
815
|
+
let bestJob: { id: string; roleId: string; task: string; type: JobType; status: JobStatus; createdAt: string; output?: string } | undefined;
|
|
816
|
+
|
|
817
|
+
for (const jobId of jobIds) {
|
|
818
|
+
if (this.jobs.has(jobId)) continue; // already in memory
|
|
819
|
+
|
|
820
|
+
const events = ActivityStream.readAll(jobId);
|
|
821
|
+
const startEvent = events.find(e => e.type === 'job:start');
|
|
822
|
+
if (!startEvent || (startEvent.data.sessionId as string) !== sessionId) continue;
|
|
823
|
+
|
|
824
|
+
const doneEvent = events.find(e => e.type === 'job:done');
|
|
825
|
+
const errorEvent = events.find(e => e.type === 'job:error');
|
|
826
|
+
const awaitingEvent = events.find(e => e.type === 'job:awaiting_input');
|
|
827
|
+
const status: JobStatus = awaitingEvent && !doneEvent ? 'awaiting_input'
|
|
828
|
+
: doneEvent ? 'done'
|
|
829
|
+
: errorEvent ? 'error'
|
|
830
|
+
: 'done';
|
|
831
|
+
|
|
832
|
+
const candidate = {
|
|
833
|
+
id: jobId,
|
|
834
|
+
roleId: startEvent.roleId,
|
|
835
|
+
task: startEvent.data.task as string ?? '',
|
|
836
|
+
type: (startEvent.data.type as string ?? 'assign') as JobType,
|
|
837
|
+
status,
|
|
838
|
+
createdAt: startEvent.ts,
|
|
839
|
+
output: doneEvent?.data?.output as string | undefined,
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
if (!bestJob || candidate.createdAt > bestJob.createdAt) {
|
|
843
|
+
bestJob = candidate;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (!bestJob) return undefined;
|
|
848
|
+
|
|
849
|
+
// Reconstruct a minimal Job in memory so replyToJob can work
|
|
850
|
+
const stream = new ActivityStream(bestJob.id, bestJob.roleId);
|
|
851
|
+
const job: Job = {
|
|
852
|
+
id: bestJob.id,
|
|
853
|
+
type: bestJob.type,
|
|
854
|
+
roleId: bestJob.roleId,
|
|
855
|
+
task: bestJob.task,
|
|
856
|
+
status: bestJob.status,
|
|
857
|
+
stream,
|
|
858
|
+
abort: () => {},
|
|
859
|
+
childJobIds: [],
|
|
860
|
+
createdAt: bestJob.createdAt,
|
|
861
|
+
sessionId,
|
|
862
|
+
result: bestJob.output ? { output: bestJob.output, turns: 0, totalTokens: { input: 0, output: 0 }, toolCalls: [], dispatches: [] } : undefined,
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
this.jobs.set(job.id, job);
|
|
866
|
+
console.log(`[JobManager] Recovered job ${job.id} for session ${sessionId} (status: ${job.status})`);
|
|
867
|
+
return job;
|
|
868
|
+
} catch (err) {
|
|
869
|
+
console.warn(`[JobManager] Failed to recover job from streams:`, err);
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
802
872
|
}
|
|
803
873
|
}
|
|
804
874
|
|