tycono 0.1.89-beta.0 → 0.1.89

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.
@@ -4,7 +4,7 @@ import { buildOrgTree } from '../engine/org-tree.js';
4
4
  import { validateDispatch, validateConsult } from '../engine/authority-validator.js';
5
5
  import { createRunner } from '../engine/runners/index.js';
6
6
  import type { ExecutionRunner } from '../engine/runners/types.js';
7
- import { setActivity, updateActivity, completeActivity } from './activity-tracker.js';
7
+ import { setActivity, updateActivity, completeActivity, markAwaitingInput } from './activity-tracker.js';
8
8
  import type { RunnerResult } from '../engine/runners/types.js';
9
9
  import { estimateCost } from './pricing.js';
10
10
  import { readConfig, getConversationLimits, resolveCodeRoot } from './company-config.js';
@@ -13,10 +13,10 @@ import { earnCoinsInternal } from '../routes/coins.js';
13
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
- /* ─── Types ──────────────────────────────── */
16
+ /* ─── Types (re-export from shared contract) ─── */
17
17
 
18
- export type JobType = 'assign' | 'wave' | 'session-message' | 'consult';
19
- export type JobStatus = 'running' | 'done' | 'error' | 'awaiting_input';
18
+ export { type JobType, type JobStatus, type JobInfo, isJobActive, canTransition, jobStatusToRoleStatus } from '../../../shared/types';
19
+ import { type JobType, type JobStatus, type JobInfo, isJobActive, canTransition } from '../../../shared/types';
20
20
 
21
21
  export interface Job {
22
22
  id: string;
@@ -41,22 +41,11 @@ export interface Job {
41
41
  sessionId?: string;
42
42
  /** PSM-003: Allocated ports for this job's dev servers */
43
43
  ports?: PortAllocation;
44
+ /** Trace ID — top-level jobId, inherited by all children for full chain tracking */
45
+ traceId?: string;
44
46
  }
45
47
 
46
- export interface JobInfo {
47
- id: string;
48
- type: JobType;
49
- roleId: string;
50
- task: string;
51
- status: JobStatus;
52
- parentJobId?: string;
53
- childJobIds: string[];
54
- createdAt: string;
55
- /** Which role should respond when status is awaiting_input */
56
- targetRole?: string;
57
- /** Final output text (available when status is done) */
58
- output?: string;
59
- }
48
+ /* JobInfo — imported from shared/types.ts */
60
49
 
61
50
  export interface StartJobParams {
62
51
  type: JobType;
@@ -162,7 +151,14 @@ class JobManager {
162
151
  }
163
152
  }
164
153
 
165
- const stream = new ActivityStream(jobId, params.roleId, params.parentJobId);
154
+ // Resolve traceId: top-level jobs use their own ID, children inherit from parent
155
+ let traceId = jobId;
156
+ if (params.parentJobId) {
157
+ const parentJob = this.jobs.get(params.parentJobId);
158
+ traceId = parentJob?.traceId ?? params.parentJobId;
159
+ }
160
+
161
+ const stream = new ActivityStream(jobId, params.roleId, params.parentJobId, traceId);
166
162
 
167
163
  const job: Job = {
168
164
  id: jobId,
@@ -177,6 +173,7 @@ class JobManager {
177
173
  createdAt: new Date().toISOString(),
178
174
  targetRoles: params.targetRoles,
179
175
  sessionId: params.sessionId,
176
+ traceId,
180
177
  };
181
178
 
182
179
  this.jobs.set(jobId, job);
@@ -207,9 +204,11 @@ class JobManager {
207
204
  // Emit job:start
208
205
  job.stream.emit('job:start', params.roleId, {
209
206
  jobId: job.id,
207
+ traceId: job.traceId,
210
208
  type: params.type,
211
209
  task: params.task,
212
210
  sourceRole: params.sourceRole ?? 'ceo',
211
+ ...(params.parentJobId && { parentJobId: params.parentJobId }),
213
212
  ...(params.sessionId && { sessionId: params.sessionId }),
214
213
  });
215
214
 
@@ -241,7 +240,7 @@ class JobManager {
241
240
  let hardLimitReached = false;
242
241
 
243
242
  // Build team status snapshot: which roles are currently busy
244
- const teamStatus: Record<string, { status: string; task?: string }> = {};
243
+ const teamStatus: import('../../../shared/types').TeamStatus = {};
245
244
  for (const [, j] of this.jobs) {
246
245
  if (j.status === 'running' && j.id !== job.id) {
247
246
  teamStatus[j.roleId] = { status: 'working', task: j.task };
@@ -412,6 +411,13 @@ class JobManager {
412
411
  handle.abort();
413
412
  }
414
413
  },
414
+ onPromptAssembled: (systemPrompt, userTask) => {
415
+ job.stream.emit('trace:prompt', params.roleId, {
416
+ systemPrompt,
417
+ userTask,
418
+ systemPromptLength: systemPrompt.length,
419
+ });
420
+ },
415
421
  onError: (error) => {
416
422
  job.stream.emit('stderr', params.roleId, { message: error });
417
423
  },
@@ -439,6 +445,14 @@ class JobManager {
439
445
  model ?? '',
440
446
  );
441
447
 
448
+ // Trace: capture full response before truncation
449
+ job.stream.emit('trace:response', params.roleId, {
450
+ fullOutput: result.output,
451
+ outputLength: result.output.length,
452
+ turns: result.turns,
453
+ tokens: result.totalTokens,
454
+ });
455
+
442
456
  const doneData = {
443
457
  output: result.output.slice(-1000),
444
458
  turns: result.turns,
@@ -454,6 +468,7 @@ class JobManager {
454
468
  if (hardLimitReached) {
455
469
  job.status = 'awaiting_input';
456
470
  job.targetRole = targetRole;
471
+ markAwaitingInput(params.roleId);
457
472
  const question = `[Turn limit] ${harnessTurnCount}턴 도달 (hardLimit: ${limits.hardLimit}). 계속 진행할까요?`;
458
473
  job.stream.emit('job:awaiting_input', params.roleId, {
459
474
  ...doneData,
@@ -468,6 +483,7 @@ class JobManager {
468
483
  else if (!params.isContinuation && hasQuestion(result.output)) {
469
484
  job.status = 'awaiting_input';
470
485
  job.targetRole = targetRole;
486
+ markAwaitingInput(params.roleId);
471
487
  job.stream.emit('job:awaiting_input', params.roleId, {
472
488
  ...doneData,
473
489
  question: result.output.trim().split('\n').slice(-5).join('\n'),
@@ -532,6 +548,7 @@ class JobManager {
532
548
  const targetRole = resolveTargetRole(params.sourceRole, params.parentJobId, this.jobs);
533
549
  job.status = 'awaiting_input';
534
550
  job.targetRole = targetRole;
551
+ markAwaitingInput(params.roleId);
535
552
  const question = `[Turn limit] ${harnessTurnCount}턴 도달 (hardLimit: ${limits.hardLimit}). 계속 진행할까요?`;
536
553
  job.stream.emit('job:awaiting_input', params.roleId, {
537
554
  question,
@@ -696,11 +713,12 @@ class JobManager {
696
713
  };
697
714
  }
698
715
 
699
- /** List jobs with optional filter */
700
- listJobs(filter?: { status?: JobStatus; roleId?: string }): JobInfo[] {
716
+ /** List jobs with optional filter. Use active:true to get running + awaiting_input. */
717
+ listJobs(filter?: { status?: JobStatus; roleId?: string; active?: boolean }): JobInfo[] {
701
718
  const result: JobInfo[] = [];
702
719
 
703
720
  for (const job of this.jobs.values()) {
721
+ if (filter?.active && !isJobActive(job.status)) continue;
704
722
  if (filter?.status && job.status !== filter.status) continue;
705
723
  if (filter?.roleId && job.roleId !== filter.roleId) continue;
706
724
  result.push({
@@ -722,7 +740,7 @@ class JobManager {
722
740
  /** Abort a running or awaiting_input job */
723
741
  abortJob(id: string): boolean {
724
742
  const job = this.jobs.get(id);
725
- if (!job || (job.status !== 'running' && job.status !== 'awaiting_input')) return false;
743
+ if (!job || !isJobActive(job.status)) return false;
726
744
 
727
745
  if (job.status === 'running') job.abort();
728
746
  job.status = 'error';
@@ -775,10 +793,10 @@ class JobManager {
775
793
  return newJob;
776
794
  }
777
795
 
778
- /** Get the active (running) job for a given role */
796
+ /** Get the active (running or awaiting_input) job for a given role */
779
797
  getActiveJobForRole(roleId: string): Job | undefined {
780
798
  for (const job of this.jobs.values()) {
781
- if (job.roleId === roleId && job.status === 'running') {
799
+ if (job.roleId === roleId && isJobActive(job.status)) {
782
800
  return job;
783
801
  }
784
802
  }
@@ -792,7 +810,7 @@ class JobManager {
792
810
  for (const job of this.jobs.values()) {
793
811
  if (job.sessionId === sessionId) {
794
812
  // Prefer running or awaiting_input jobs
795
- if (job.status === 'running' || job.status === 'awaiting_input') {
813
+ if (isJobActive(job.status)) {
796
814
  if (!active || job.createdAt > active.createdAt) {
797
815
  active = job;
798
816
  }
@@ -8,7 +8,7 @@ import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { execSync } from 'node:child_process';
10
10
  import { writeConfig } from './company-config.js';
11
- import { mergePreferences } from './preferences.js';
11
+ import { mergePreferences, type CharacterAppearance } from './preferences.js';
12
12
  import type { CompanyConfig } from './company-config.js';
13
13
 
14
14
  const __filename = fileURLToPath(import.meta.url);
@@ -415,7 +415,7 @@ export function scaffold(config: ScaffoldConfig): string[] {
415
415
  // Set default appearances for team roles
416
416
  if (config.team !== 'custom') {
417
417
  const roles = loadTeam(config.team);
418
- const appearances: Record<string, unknown> = {};
418
+ const appearances: Record<string, CharacterAppearance> = {};
419
419
  for (const role of roles) {
420
420
  const def = DEFAULT_ROLE_APPEARANCES[role.id];
421
421
  if (def) appearances[role.id] = def;
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { COMPANY_ROOT } from './file-reader.js';
4
- import type { ActivityEvent } from './activity-stream.js';
4
+ import { type ActivityEvent, type SessionSource, type SessionStatus, type MessageStatus, isMessageTerminal } from '../../../shared/types';
5
5
 
6
6
  /* ─── Types ─────────────────────────────── */
7
7
 
@@ -23,7 +23,7 @@ export interface Message {
23
23
  from: 'ceo' | 'role';
24
24
  content: string;
25
25
  type: 'conversation' | 'directive' | 'system';
26
- status?: 'streaming' | 'done' | 'error' | 'awaiting_input';
26
+ status?: MessageStatus;
27
27
  timestamp: string;
28
28
  attachments?: ImageAttachment[];
29
29
 
@@ -43,16 +43,13 @@ export interface Message {
43
43
  knowledgeDebt?: Array<{ type: string; file?: string; message: string }>;
44
44
  }
45
45
 
46
- /** How this session was created */
47
- export type SessionSource = 'chat' | 'wave' | 'dispatch';
48
-
49
46
  export interface Session {
50
47
  id: string;
51
48
  roleId: string;
52
49
  title: string;
53
50
  mode: 'talk' | 'do';
54
51
  messages: Message[];
55
- status: 'active' | 'closed';
52
+ status: SessionStatus;
56
53
  createdAt: string;
57
54
  updatedAt: string;
58
55
 
@@ -213,7 +210,7 @@ export function updateMessage(sessionId: string, messageId: string, updates: Mes
213
210
  if (updates.knowledgeDebt !== undefined) msg.knowledgeDebt = updates.knowledgeDebt;
214
211
  session.updatedAt = new Date().toISOString();
215
212
 
216
- if (updates.status === 'done' || updates.status === 'error') {
213
+ if (updates.status && isMessageTerminal(updates.status)) {
217
214
  writeImmediate(session);
218
215
  } else {
219
216
  debouncedWrite(session);
@@ -1,5 +1,6 @@
1
1
  import { ActivityStream, type ActivityEvent, type ActivitySubscriber } from './activity-stream.js';
2
2
  import type { Job } from './job-manager.js';
3
+ import { isJobActive } from '../../../shared/types';
3
4
  import type { Response } from 'express';
4
5
 
5
6
  /* ─── Types ──────────────────────────────── */
@@ -125,9 +126,11 @@ class WaveMultiplexer {
125
126
  }
126
127
 
127
128
  // Phase 2: Subscribe to live events for running jobs
129
+ // sendNotification=true ensures wave:role-attached is sent for each active job,
130
+ // so the client knows which roles are running. History replay is deduped by sentEvents.
128
131
  for (const [, job] of jobs) {
129
- if (job.status === 'running' || job.status === 'awaiting_input') {
130
- this.subscribeJobToClient(client, job, false);
132
+ if (isJobActive(job.status)) {
133
+ this.subscribeJobToClient(client, job, true);
131
134
  }
132
135
  }
133
136
  }
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  import { COMPANY_ROOT } from './file-reader.js';
8
8
  import { ActivityStream, type ActivityEvent } from './activity-stream.js';
9
9
  import { jobManager } from './job-manager.js';
10
+ import { type WaveRoleStatus, eventTypeToJobStatus } from '../../../shared/types';
10
11
 
11
12
  /* ─── Find wave file ──────────────────────── */
12
13
 
@@ -148,9 +149,7 @@ export function updateFollowUpInWave(waveId: string, jobId: string, roleId: stri
148
149
 
149
150
  const newEvents = ActivityStream.readAll(jobId);
150
151
  const doneEvent = newEvents.find(e => e.type === 'job:done' || e.type === 'job:error' || e.type === 'job:awaiting_input');
151
- const status = doneEvent?.type === 'job:done' ? 'done'
152
- : doneEvent?.type === 'job:error' ? 'error'
153
- : doneEvent?.type === 'job:awaiting_input' ? 'awaiting_input' : 'running';
152
+ const status: WaveRoleStatus = doneEvent ? eventTypeToJobStatus(doneEvent.type) as WaveRoleStatus : 'running' as any;
154
153
 
155
154
  // Collect child jobs
156
155
  const childJobs: Array<{ roleId: string; roleName: string; jobId: string; status: string; events: ReturnType<typeof ActivityStream.readAll> }> = [];
@@ -159,8 +158,8 @@ export function updateFollowUpInWave(waveId: string, jobId: string, roleId: stri
159
158
  const childJobId = e.data.childJobId as string;
160
159
  const targetRoleId = (e.data.targetRoleId as string) ?? 'unknown';
161
160
  const childEvents = ActivityStream.readAll(childJobId);
162
- const childDone = childEvents.find(ce => ce.type === 'job:done' || ce.type === 'job:error');
163
- const childStatus = childDone?.type === 'job:done' ? 'done' : childDone?.type === 'job:error' ? 'error' : 'unknown';
161
+ const childDone = childEvents.find(ce => ce.type === 'job:done' || ce.type === 'job:error' || ce.type === 'job:awaiting_input');
162
+ const childStatus: WaveRoleStatus = childDone ? eventTypeToJobStatus(childDone.type) as WaveRoleStatus : 'unknown';
164
163
  childJobs.push({ roleId: targetRoleId, roleName: targetRoleId, jobId: childJobId, status: childStatus, events: childEvents });
165
164
  }
166
165
  }