tycono 0.1.69 → 0.1.70

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.69",
3
+ "version": "0.1.70",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,22 @@ function writeCoins(data: CoinsData) {
46
46
  writeFileSync(COINS_FILE(), JSON.stringify(data, null, 2) + '\n');
47
47
  }
48
48
 
49
+ /* ── Internal API (for server-side use) ── */
50
+
51
+ export function earnCoinsInternal(amount: number, reason: string, ref?: string): { balance: number; skipped: boolean } {
52
+ const data = readCoins();
53
+ // Idempotency
54
+ if (ref && data.transactions.some(t => t.ref === ref && t.amount > 0)) {
55
+ return { balance: data.balance, skipped: true };
56
+ }
57
+ const tx: CoinTransaction = { ts: new Date().toISOString(), amount, reason, ref };
58
+ data.balance += amount;
59
+ data.totalEarned += amount;
60
+ data.transactions.push(tx);
61
+ writeCoins(data);
62
+ return { balance: data.balance, skipped: false };
63
+ }
64
+
49
65
  /* ── Routes ── */
50
66
 
51
67
  // GET /api/coins — current balance + summary
@@ -15,6 +15,7 @@ import {
15
15
  } from '../services/session-store.js';
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
+ import { earnCoinsInternal } from './coins.js';
18
19
 
19
20
  /* ─── Runner — lazy, re-created when engine changes ── */
20
21
 
@@ -451,8 +452,22 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
451
452
  if (!fs.existsSync(wavesDir)) {
452
453
  fs.mkdirSync(wavesDir, { recursive: true });
453
454
  }
454
- const hhmmss = now.toTimeString().slice(0, 8).replace(/:/g, '');
455
- const baseName = `${dateStr.replace(/-/g, '')}-${hhmmss}-wave`;
455
+
456
+ // Dedup: if waveId matches an existing file, overwrite instead of creating new
457
+ let baseName: string;
458
+ if (waveId) {
459
+ const existing = fs.readdirSync(wavesDir).find(f => {
460
+ if (!f.endsWith('.json')) return false;
461
+ try {
462
+ const data = JSON.parse(fs.readFileSync(path.join(wavesDir, f), 'utf-8'));
463
+ return data.waveId === waveId || data.id === waveId;
464
+ } catch { return false; }
465
+ });
466
+ baseName = existing ? existing.replace('.json', '') : waveId;
467
+ } else {
468
+ const hhmmss = now.toTimeString().slice(0, 8).replace(/:/g, '');
469
+ baseName = `${dateStr.replace(/-/g, '')}-${hhmmss}-wave`;
470
+ }
456
471
  const jsonPath = path.join(wavesDir, `${baseName}.json`);
457
472
 
458
473
  const waveJson = {
@@ -467,6 +482,14 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
467
482
  };
468
483
  fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
469
484
 
485
+ // EC-012: Wave completion bonus (participating roles × 500 coins)
486
+ const roleCount = rolesData.length;
487
+ if (roleCount > 0) {
488
+ try {
489
+ earnCoinsInternal(roleCount * 500, `Wave done: ${roleCount} roles`, `wave:${baseName}`);
490
+ } catch { /* non-critical */ }
491
+ }
492
+
470
493
  jsonResponse(res, 200, { ok: true, path: `operations/waves/${baseName}.json` });
471
494
  }
472
495
 
@@ -9,6 +9,7 @@ import type { RunnerResult } from '../engine/runners/types.js';
9
9
  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
+ import { earnCoinsInternal } from '../routes/coins.js';
12
13
  import { getSession, updateMessage as updateSessionMessage, appendMessageEvent } from './session-store.js';
13
14
 
14
15
  /* ─── Types ──────────────────────────────── */
@@ -438,6 +439,16 @@ class JobManager {
438
439
  if (job.sessionId) {
439
440
  this.finalizeSessionMessage(job, 'done', result);
440
441
  }
442
+
443
+ // EC-011: Job completion bonus (only for top-level jobs, not child dispatches)
444
+ if (!params.parentJobId && result) {
445
+ const totalTokens = (result.totalTokens?.input ?? 0) + (result.totalTokens?.output ?? 0);
446
+ const bonus = Math.min(2000, Math.max(500, Math.round(totalTokens / 500)));
447
+ try {
448
+ earnCoinsInternal(bonus, `Job done: ${params.roleId}`, `job:${job.id}`);
449
+ } catch { /* non-critical */ }
450
+ }
451
+
441
452
  // Cleanup orphaned child jobs (awaiting_input with no parent to respond)
442
453
  this.cleanupOrphanedChildren(job.id);
443
454
  }
@@ -533,6 +544,10 @@ class JobManager {
533
544
  turns: result.turns,
534
545
  tokens: result.totalTokens,
535
546
  }),
547
+ // KP-006: Include knowledge debt in session message
548
+ ...(job.knowledgeDebt && job.knowledgeDebt.length > 0 && {
549
+ knowledgeDebt: job.knowledgeDebt.map(d => ({ type: d.type, file: d.file, message: d.message })),
550
+ }),
536
551
  });
537
552
  }
538
553
 
@@ -39,6 +39,8 @@ export interface Message {
39
39
  /** Execution stats */
40
40
  turns?: number;
41
41
  tokens?: { input: number; output: number };
42
+ /** KP-006: Knowledge debt warnings from Post-K check */
43
+ knowledgeDebt?: Array<{ type: string; file?: string; message: string }>;
42
44
  }
43
45
 
44
46
  /** How this session was created */
@@ -193,7 +195,7 @@ export function addMessage(sessionId: string, msg: Message, streaming = false):
193
195
  }
194
196
 
195
197
  /** Fields that can be updated on a message */
196
- export type MessageUpdate = Partial<Pick<Message, 'content' | 'status' | 'turns' | 'tokens' | 'dispatches' | 'readOnly'>>;
198
+ export type MessageUpdate = Partial<Pick<Message, 'content' | 'status' | 'turns' | 'tokens' | 'dispatches' | 'readOnly' | 'knowledgeDebt'>>;
197
199
 
198
200
  export function updateMessage(sessionId: string, messageId: string, updates: MessageUpdate): Session | undefined {
199
201
  const session = cache.get(sessionId);
@@ -208,6 +210,7 @@ export function updateMessage(sessionId: string, messageId: string, updates: Mes
208
210
  if (updates.tokens !== undefined) msg.tokens = updates.tokens;
209
211
  if (updates.dispatches !== undefined) msg.dispatches = updates.dispatches;
210
212
  if (updates.readOnly !== undefined) msg.readOnly = updates.readOnly;
213
+ if (updates.knowledgeDebt !== undefined) msg.knowledgeDebt = updates.knowledgeDebt;
211
214
  session.updatedAt = new Date().toISOString();
212
215
 
213
216
  if (updates.status === 'done' || updates.status === 'error') {