tycono 0.1.73 → 0.1.74-beta.1

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 CHANGED
@@ -1,11 +1,12 @@
1
1
  <p align="center">
2
- <img src=".github/assets/hero-office.png" alt="Tycono — AI Office" width="640" />
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> &middot;
19
- <a href="#quick-start">Quick Start</a> &middot;
20
- <a href="#how-it-works">How It Works</a> &middot;
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 create and run an AI-powered organization. Define roles (CTO, PM, Engineer...), assign them AI agents, and watch them collaborate through a real-time dashboard.
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
- ## Quick Start
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
- | **Structure** | One agent, one context | Multiple roles with org hierarchy |
42
- | **Knowledge** | Loses context between sessions | Persistent, file-based knowledge system (AKB) |
43
- | **Authority** | Can do anything | Scoped — each role has boundaries |
44
- | **Delegation** | Manual prompt chaining | Automatic dispatch through org chart |
45
- | **Visibility** | Terminal output | Real-time isometric office dashboard |
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
- ## What You Get
85
+ ### Office View — Watch your AI team
48
86
 
49
- - **Role-based AI agents** Each role has its own persona, authority scope, and knowledge boundaries
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/sidepanel-chat.png" alt="Tycono Dashboard" width="640" />
90
+ <img src=".github/assets/hero-office.png" alt="Office View" width="640" />
58
91
  </p>
59
92
 
60
- ## Requirements
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
- - Node.js >= 18
63
- - [Anthropic API key](https://console.anthropic.com/)
98
+ ### Pro View — Manage your AI company
64
99
 
65
- ## Team Templates
100
+ A Slack-style professional dashboard for serious work. Chats, Wave dispatch, Decisions log, Knowledge graph.
66
101
 
67
- During setup, pick a template or build your own:
102
+ <p align="center">
103
+ <img src=".github/assets/pro-view.png" alt="Pro View" width="640" />
104
+ </p>
68
105
 
69
- | Template | Roles | Best For |
70
- |----------|-------|----------|
71
- | **Startup** | CTO + PM + Engineer | Product development |
72
- | **Research** | Lead Researcher + Analyst + Writer | Analysis & reports |
73
- | **Agency** | Creative Director + Designer + Developer | Client projects |
74
- | **Custom** | Start empty, hire as you go | Full control |
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 instructions via dashboard
145
+ └── Give a directive via Wave or direct chat
81
146
  └── Context Engine routes to the right Role
82
- └── Role reads its knowledge, executes with authority
83
- └── Results flow back up the org chart
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 capabilities
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 entry point (auto-managed)
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, waves
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 tycono. An AI company that builds itself.</sub>
235
+ <sub>Built with Tycono. An AI company that builds itself.</sub>
155
236
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.73",
3
+ "version": "0.1.74-beta.1",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- jsonResponse(res, 200, { jobId: job.id, ...(sessionId && { sessionId }) });
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: data.roles?.length ?? 0,
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 is required' });
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 newJob = jobManager.replyToJob(job.id, message, responderRole);
214
- if (!newJob) {
215
- res.status(400).json({ error: 'Job not in a replyable state' });
216
- return;
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
  });
@@ -157,4 +157,9 @@ export class ActivityStream {
157
157
  .filter(f => f.endsWith('.jsonl'))
158
158
  .map(f => f.replace('.jsonl', ''));
159
159
  }
160
+
161
+ /** Get the streams directory path */
162
+ static getStreamDir(): string {
163
+ return streamsDir();
164
+ }
160
165
  }
@@ -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