@voybio/ace-swarm 0.2.0 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +15 -15
  2. package/assets/agent-state/EVIDENCE_LOG.md +1 -1
  3. package/assets/agent-state/STATUS.md +2 -2
  4. package/dist/ace-autonomy.js +38 -1
  5. package/dist/ace-context.js +8 -0
  6. package/dist/ace-server-instructions.js +55 -19
  7. package/dist/ace-state-resolver.d.ts +18 -0
  8. package/dist/ace-state-resolver.js +106 -0
  9. package/dist/cli.js +67 -0
  10. package/dist/handoff-registry.js +11 -7
  11. package/dist/helpers.js +74 -8
  12. package/dist/job-scheduler.js +94 -44
  13. package/dist/run-ledger.js +3 -4
  14. package/dist/server.d.ts +1 -1
  15. package/dist/server.js +1 -1
  16. package/dist/shared.d.ts +1 -1
  17. package/dist/status-events.js +12 -14
  18. package/dist/store/bootstrap-store.js +20 -9
  19. package/dist/store/materializers/context-snapshot-materializer.d.ts +10 -0
  20. package/dist/store/materializers/context-snapshot-materializer.js +51 -0
  21. package/dist/store/materializers/host-file-materializer.d.ts +6 -0
  22. package/dist/store/materializers/host-file-materializer.js +13 -0
  23. package/dist/store/materializers/projection-manager.d.ts +14 -0
  24. package/dist/store/materializers/projection-manager.js +73 -0
  25. package/dist/store/materializers/scheduler-projection-materializer.d.ts +16 -0
  26. package/dist/store/materializers/scheduler-projection-materializer.js +48 -0
  27. package/dist/store/repositories/context-snapshot-repository.d.ts +46 -0
  28. package/dist/store/repositories/context-snapshot-repository.js +105 -0
  29. package/dist/store/repositories/local-model-runtime-repository.d.ts +98 -0
  30. package/dist/store/repositories/local-model-runtime-repository.js +165 -0
  31. package/dist/store/repositories/scheduler-repository.d.ts +21 -39
  32. package/dist/store/repositories/scheduler-repository.js +123 -93
  33. package/dist/store/repositories/todo-repository.d.ts +4 -0
  34. package/dist/store/repositories/todo-repository.js +50 -0
  35. package/dist/store/state-reader.d.ts +8 -1
  36. package/dist/store/state-reader.js +12 -1
  37. package/dist/store/store-artifacts.js +31 -5
  38. package/dist/store/store-authority-audit.d.ts +30 -0
  39. package/dist/store/store-authority-audit.js +448 -0
  40. package/dist/store/types.d.ts +2 -0
  41. package/dist/store/types.js +1 -0
  42. package/dist/todo-state.js +179 -11
  43. package/dist/tools-files.js +2 -1
  44. package/dist/tools-framework.js +60 -0
  45. package/dist/tools-memory.js +69 -34
  46. package/dist/tools-todo.js +1 -1
  47. package/dist/tui/agent-worker.d.ts +1 -1
  48. package/dist/tui/agent-worker.js +5 -3
  49. package/dist/tui/chat.d.ts +19 -0
  50. package/dist/tui/chat.js +275 -9
  51. package/dist/tui/commands.d.ts +2 -0
  52. package/dist/tui/commands.js +62 -0
  53. package/dist/tui/dashboard.d.ts +5 -0
  54. package/dist/tui/dashboard.js +38 -2
  55. package/dist/tui/index.d.ts +5 -0
  56. package/dist/tui/index.js +146 -2
  57. package/dist/tui/input.js +5 -0
  58. package/dist/tui/layout.d.ts +24 -0
  59. package/dist/tui/layout.js +76 -2
  60. package/dist/tui/local-model-contract.d.ts +50 -0
  61. package/dist/tui/local-model-contract.js +272 -0
  62. package/dist/vericify-bridge.js +3 -4
  63. package/dist/vericify-context.js +18 -6
  64. package/package.json +1 -1
package/dist/tui/chat.js CHANGED
@@ -7,9 +7,12 @@
7
7
  import { diagnoseChatRuntimeConfig, } from "./openai-compatible.js";
8
8
  import { estimateTokenCount } from "./telemetry.js";
9
9
  import { EventEmitter } from "node:events";
10
- import { existsSync } from "node:fs";
10
+ import { randomUUID } from "node:crypto";
11
11
  import { resolve } from "node:path";
12
12
  import { ModelBridge } from "../model-bridge.js";
13
+ import { resolveAceStateLayout } from "../ace-state-resolver.js";
14
+ import { applyEvidenceGuardrail, buildAcePreflightPacket, buildBridgeTaskInput, buildContinuityRecord, buildStartupNudge, mapBridgeResultToRuntimeStatus, nextActivationLedger, } from "./local-model-contract.js";
15
+ import { withLocalModelRuntimeRepository, } from "../store/repositories/local-model-runtime-repository.js";
13
16
  export class ChatSession extends EventEmitter {
14
17
  clients;
15
18
  telemetry;
@@ -32,6 +35,11 @@ export class ChatSession extends EventEmitter {
32
35
  aceBridge;
33
36
  activeAceBridge = false;
34
37
  providerBaseUrls;
38
+ sessionId = randomUUID();
39
+ createdAt = Date.now();
40
+ lastActiveAt = this.createdAt;
41
+ currentRuntimeStatus = null;
42
+ activationLedger = null;
35
43
  constructor(clients, telemetry, options) {
36
44
  super();
37
45
  this.clients = clients;
@@ -63,6 +71,35 @@ export class ChatSession extends EventEmitter {
63
71
  getDisplayMessages() {
64
72
  return this.displayMessages;
65
73
  }
74
+ getSessionId() {
75
+ return this.sessionId;
76
+ }
77
+ getCreatedAt() {
78
+ return this.createdAt;
79
+ }
80
+ getLastActiveAt() {
81
+ return this.lastActiveAt;
82
+ }
83
+ getCurrentRuntimeStatus() {
84
+ return this.currentRuntimeStatus ?? undefined;
85
+ }
86
+ hasMeaningfulTranscript() {
87
+ return this.messages.some((message) => message.role !== "system" && String(message.content ?? "").trim().length > 0);
88
+ }
89
+ getTranscriptExcerpt(maxMessages = 6) {
90
+ const excerpt = this.messages
91
+ .filter((message) => message.role !== "system")
92
+ .slice(-maxMessages)
93
+ .map((message) => `${message.role}: ${oneLine(String(message.content ?? ""))}`)
94
+ .join(" | ");
95
+ return excerpt ? clip(excerpt, 720) : undefined;
96
+ }
97
+ getTranscriptSummary() {
98
+ const lastAssistant = [...this.messages]
99
+ .reverse()
100
+ .find((message) => message.role === "assistant" && String(message.content ?? "").trim().length > 0);
101
+ return lastAssistant ? clip(oneLine(String(lastAssistant.content ?? "")), 240) : undefined;
102
+ }
66
103
  /** Is currently streaming a response? */
67
104
  isStreaming() {
68
105
  return this.streaming;
@@ -115,6 +152,7 @@ export class ChatSession extends EventEmitter {
115
152
  timestamp: Date.now(),
116
153
  tokens: estimateTokenCount(text),
117
154
  });
155
+ this.noteActivity();
118
156
  this.emit("updated");
119
157
  // Start streaming response
120
158
  this.streaming = true;
@@ -161,6 +199,7 @@ export class ChatSession extends EventEmitter {
161
199
  timestamp: Date.now(),
162
200
  tokens: estimateTokenCount(this.currentStreamText),
163
201
  });
202
+ this.noteActivity();
164
203
  }
165
204
  catch (err) {
166
205
  if (err?.name === "AbortError") {
@@ -210,6 +249,7 @@ export class ChatSession extends EventEmitter {
210
249
  clear() {
211
250
  this.messages = [];
212
251
  this.displayMessages = [];
252
+ this.currentRuntimeStatus = null;
213
253
  if (this.systemPrompt) {
214
254
  this.messages.push({ role: "system", content: this.systemPrompt });
215
255
  this.displayMessages.push({
@@ -274,10 +314,7 @@ export class ChatSession extends EventEmitter {
274
314
  return false;
275
315
  if (!this.workspaceRoot)
276
316
  return false;
277
- if (!existsSync(resolve(this.workspaceRoot, ".agents", "ACE", "agent-state", "TASK.md"))) {
278
- return false;
279
- }
280
- return true;
317
+ return resolveAceStateLayout(this.workspaceRoot).isAcePresent;
281
318
  }
282
319
  buildBridgeTask() {
283
320
  const recentConversation = this.messages
@@ -293,39 +330,206 @@ export class ChatSession extends EventEmitter {
293
330
  content,
294
331
  timestamp: Date.now(),
295
332
  });
333
+ this.noteActivity();
296
334
  this.emit("updated");
297
335
  }
336
+ noteActivity() {
337
+ this.lastActiveAt = Date.now();
338
+ }
339
+ buildRuntimeStatusPacket(input) {
340
+ return {
341
+ session_id: this.sessionId,
342
+ process_id: process.pid,
343
+ turn_count: this.messages.filter((message) => message.role === "user").length,
344
+ role: input.role,
345
+ bridge_status: input.bridgeStatus,
346
+ preflight_state: input.preflightState,
347
+ approval_state: this.currentRuntimeStatus?.approval_state ?? "not_required",
348
+ current_task: input.task,
349
+ recommended_next_action: input.recommendedNextAction,
350
+ blocked_reason: input.blockedReason,
351
+ updated_at: Date.now(),
352
+ };
353
+ }
354
+ setRuntimeStatus(status) {
355
+ this.currentRuntimeStatus = status;
356
+ this.emit("runtime_status", status);
357
+ }
358
+ setApprovalState(state) {
359
+ if (!this.currentRuntimeStatus) {
360
+ return false;
361
+ }
362
+ const next = {
363
+ ...this.currentRuntimeStatus,
364
+ approval_state: state,
365
+ updated_at: Date.now(),
366
+ };
367
+ this.setRuntimeStatus(next);
368
+ this.persistRuntimeStatus(next);
369
+ this.displayMessages.push({
370
+ role: "system",
371
+ content: `Approval state set to: ${state}`,
372
+ timestamp: Date.now(),
373
+ });
374
+ this.emit("updated");
375
+ this.noteActivity();
376
+ return true;
377
+ }
378
+ persistRuntimeStatus(status) {
379
+ if (!this.workspaceRoot)
380
+ return;
381
+ const { updated_at: _updatedAt, ...record } = status;
382
+ void withLocalModelRuntimeRepository(this.workspaceRoot, async (repo) => {
383
+ await repo.upsertRuntimeStatus(record);
384
+ }).catch((error) => {
385
+ this.emit("error", `Runtime status persistence failed: ${formatErrorMessage(error)}`);
386
+ });
387
+ }
388
+ persistRuntimeSessionArtifacts(input) {
389
+ if (!this.workspaceRoot)
390
+ return;
391
+ void withLocalModelRuntimeRepository(this.workspaceRoot, async (repo) => {
392
+ if (input.activationLedger) {
393
+ await repo.upsertActivationLedger(input.activationLedger);
394
+ }
395
+ if (input.runtimeStatus) {
396
+ const { updated_at: _updatedAt, ...statusRecord } = input.runtimeStatus;
397
+ await repo.upsertRuntimeStatus(statusRecord);
398
+ }
399
+ if (input.continuity) {
400
+ await repo.upsertContinuityRecord(input.continuity);
401
+ }
402
+ }).catch((error) => {
403
+ this.emit("error", `ACE runtime persistence failed: ${formatErrorMessage(error)}`);
404
+ });
405
+ }
298
406
  async runAceBridge(text, startTime, streamProvider, streamModel) {
299
407
  if (!this.workspaceRoot) {
300
408
  throw new Error("ACE bridge requires a workspace root.");
301
409
  }
302
410
  this.activeAceBridge = true;
411
+ const recentConversation = this.buildBridgeTask();
412
+ const preflight = buildAcePreflightPacket({
413
+ sessionId: this.sessionId,
414
+ workspaceRoot: this.workspaceRoot,
415
+ task: recentConversation,
416
+ preferredRole: this.aceRole,
417
+ });
418
+ const executionRole = preflight.recommended_role ?? this.aceRole;
419
+ let activationLedger = nextActivationLedger(this.sessionId, this.activationLedger ?? undefined, preflight.recommended_next_action);
420
+ const startupNudge = buildStartupNudge(preflight, activationLedger);
421
+ if (startupNudge) {
422
+ activationLedger = {
423
+ ...activationLedger,
424
+ shown_nudges: uniqueStrings([...activationLedger.shown_nudges, startupNudge.id]),
425
+ };
426
+ this.pushSystemMessage(`Hint: ${startupNudge.text}`);
427
+ this.emit("startup_nudge", startupNudge);
428
+ }
429
+ let runtimeStatus = this.buildRuntimeStatusPacket({
430
+ role: executionRole,
431
+ task: text,
432
+ preflightState: preflight.preflight_state,
433
+ bridgeStatus: preflight.bridge_status,
434
+ recommendedNextAction: preflight.recommended_next_action,
435
+ blockedReason: preflight.blockers[0],
436
+ });
437
+ this.setRuntimeStatus(runtimeStatus);
438
+ this.persistRuntimeSessionArtifacts({
439
+ activationLedger,
440
+ runtimeStatus,
441
+ });
442
+ this.activationLedger = activationLedger;
443
+ if (preflight.preflight_state === "blocked") {
444
+ const blockerText = preflight.blockers.join("; ");
445
+ this.pushSystemMessage(`ACE preflight blocked: ${blockerText}`);
446
+ const blockedContinuity = buildContinuityRecord({
447
+ preflight,
448
+ role: executionRole,
449
+ bridgeStatus: runtimeStatus.bridge_status,
450
+ });
451
+ this.persistRuntimeSessionArtifacts({
452
+ activationLedger,
453
+ runtimeStatus,
454
+ continuity: blockedContinuity,
455
+ });
456
+ this.activationLedger = activationLedger;
457
+ return;
458
+ }
303
459
  let bridgeOutput = "";
460
+ const toolNames = [];
461
+ let retryAttempt = 0;
304
462
  const result = await this.aceBridge.run({
305
- task: this.buildBridgeTask(),
306
- role: this.aceRole,
463
+ task: buildBridgeTaskInput(recentConversation, preflight),
464
+ role: executionRole,
307
465
  workspace: this.workspaceRoot,
308
- tier: this.aceTier ?? (this.aceRole === "orchestrator" ? "compressed" : "brief"),
466
+ tier: this.aceTier ?? (executionRole === "orchestrator" ? "compressed" : "brief"),
309
467
  maxTurns: this.maxTurns,
310
468
  provider: streamProvider,
311
469
  model: streamModel,
312
470
  numCtx: this.numCtx,
313
471
  onThinking: (thinking) => {
314
472
  this.pushSystemMessage(`Thinking: ${thinking}`);
473
+ if (/\bretrying\b/i.test(thinking)) {
474
+ retryAttempt += 1;
475
+ runtimeStatus = {
476
+ ...runtimeStatus,
477
+ bridge_status: "retrying",
478
+ retry_state: {
479
+ attempt: retryAttempt,
480
+ max_attempts: 2,
481
+ reason: thinking,
482
+ },
483
+ updated_at: Date.now(),
484
+ };
485
+ this.setRuntimeStatus(runtimeStatus);
486
+ this.persistRuntimeStatus(runtimeStatus);
487
+ }
315
488
  },
316
489
  onToolCall: (tool, args) => {
490
+ toolNames.push(tool);
491
+ activationLedger = {
492
+ ...activationLedger,
493
+ updated_at: Date.now(),
494
+ activated_tools: uniqueStrings([...activationLedger.activated_tools, tool]),
495
+ activated_roles: uniqueStrings([...activationLedger.activated_roles, executionRole]),
496
+ accepted_nudges: tool === startupNudge?.recommended_action
497
+ ? uniqueStrings([...activationLedger.accepted_nudges, startupNudge.id])
498
+ : activationLedger.accepted_nudges,
499
+ };
500
+ runtimeStatus = {
501
+ ...runtimeStatus,
502
+ bridge_status: "running",
503
+ active_tool: tool,
504
+ active_tool_role: executionRole,
505
+ updated_at: Date.now(),
506
+ };
507
+ this.setRuntimeStatus(runtimeStatus);
508
+ this.persistRuntimeSessionArtifacts({ activationLedger, runtimeStatus });
509
+ this.activationLedger = activationLedger;
317
510
  this.pushSystemMessage(`Tool call: ${tool} ${safeStringify(args)}`);
318
511
  this.emit("tool_call", tool, args);
319
512
  },
320
513
  onToolResult: (tool, toolResult) => {
514
+ runtimeStatus = {
515
+ ...runtimeStatus,
516
+ bridge_status: toolResult.isError ? "needs_input" : "running",
517
+ active_tool: undefined,
518
+ blocked_reason: toolResult.isError ? `${tool}: ${toolResult.summary}` : undefined,
519
+ updated_at: Date.now(),
520
+ };
521
+ this.setRuntimeStatus(runtimeStatus);
522
+ this.persistRuntimeStatus(runtimeStatus);
321
523
  this.pushSystemMessage(`Tool ${toolResult.ok ? "ok" : "error"}: ${tool} — ${toolResult.summary}`);
322
524
  this.emit("tool_result", tool, toolResult);
323
525
  },
324
526
  onOutput: (output) => {
325
527
  bridgeOutput = output;
528
+ this.noteActivity();
326
529
  },
327
530
  });
328
- const assistantText = bridgeOutput.trim() || result.summary.trim() || "ACE bridge completed.";
531
+ const rawAssistantText = bridgeOutput.trim() || result.summary.trim() || "ACE bridge completed.";
532
+ const assistantText = applyEvidenceGuardrail(rawAssistantText, toolNames);
329
533
  this.messages.push({ role: "assistant", content: assistantText });
330
534
  this.displayMessages.push({
331
535
  role: "assistant",
@@ -333,9 +537,57 @@ export class ChatSession extends EventEmitter {
333
537
  timestamp: Date.now(),
334
538
  tokens: estimateTokenCount(assistantText),
335
539
  });
540
+ this.noteActivity();
336
541
  if (result.status === "needs_input") {
337
542
  this.pushSystemMessage("[Awaiting operator follow-up]");
338
543
  }
544
+ if (result.status === "failed") {
545
+ runtimeStatus = {
546
+ ...runtimeStatus,
547
+ bridge_status: "failed",
548
+ blocked_reason: assistantText,
549
+ active_tool: undefined,
550
+ updated_at: Date.now(),
551
+ };
552
+ }
553
+ else if (result.status === "max_turns") {
554
+ runtimeStatus = {
555
+ ...runtimeStatus,
556
+ bridge_status: "needs_input",
557
+ blocked_reason: "ACE bridge reached max turns before completion.",
558
+ active_tool: undefined,
559
+ updated_at: Date.now(),
560
+ };
561
+ }
562
+ else if (result.status === "needs_input") {
563
+ runtimeStatus = {
564
+ ...runtimeStatus,
565
+ bridge_status: "needs_input",
566
+ active_tool: undefined,
567
+ updated_at: Date.now(),
568
+ };
569
+ }
570
+ else {
571
+ runtimeStatus = mapBridgeResultToRuntimeStatus({
572
+ current: runtimeStatus,
573
+ summary: assistantText,
574
+ toolNames,
575
+ });
576
+ }
577
+ this.setRuntimeStatus(runtimeStatus);
578
+ const continuity = buildContinuityRecord({
579
+ preflight,
580
+ role: executionRole,
581
+ activeTool: runtimeStatus.active_tool,
582
+ bridgeStatus: runtimeStatus.bridge_status,
583
+ evidenceRefs: toolNames.map((tool) => `tool:${tool}`),
584
+ });
585
+ this.persistRuntimeSessionArtifacts({
586
+ activationLedger,
587
+ runtimeStatus,
588
+ continuity,
589
+ });
590
+ this.activationLedger = activationLedger;
339
591
  this.telemetry.recordRequest({
340
592
  startTime,
341
593
  endTime: Date.now(),
@@ -365,4 +617,18 @@ function safeStringify(value) {
365
617
  return "{}";
366
618
  }
367
619
  }
620
+ function oneLine(input) {
621
+ return input.replace(/\s+/g, " ").trim();
622
+ }
623
+ function clip(input, max) {
624
+ if (input.length <= max)
625
+ return input;
626
+ return `${input.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
627
+ }
628
+ function uniqueStrings(values) {
629
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0)));
630
+ }
631
+ function formatErrorMessage(error) {
632
+ return error instanceof Error ? error.message : String(error);
633
+ }
368
634
  //# sourceMappingURL=chat.js.map
@@ -34,9 +34,11 @@ export interface TuiController {
34
34
  createChatTab(): void;
35
35
  createLogTab(): void;
36
36
  closeCurrentTab(): void;
37
+ archiveCurrentTab(): Promise<void>;
37
38
  showMessage(text: string, level?: "info" | "warn" | "error"): void;
38
39
  clearView(): void;
39
40
  refresh(): void;
41
+ setApprovalState(state: "not_required" | "pending" | "approved" | "rejected"): boolean;
40
42
  getStatus(): Record<string, unknown>;
41
43
  quit(): void;
42
44
  sendChatMessage(text: string): Promise<void>;
@@ -304,6 +304,48 @@ export function registerBuiltinCommands(registry) {
304
304
  ctx.tui.showMessage(lines);
305
305
  },
306
306
  });
307
+ // /approve — Mark the active runtime approval as approved
308
+ registry.register({
309
+ name: "approve",
310
+ aliases: [],
311
+ description: "Approve the active runtime packet",
312
+ usage: "/approve",
313
+ handler: (_args, ctx) => {
314
+ if (!ctx.tui.setApprovalState("approved")) {
315
+ ctx.tui.showMessage("No active runtime packet to approve.", "warn");
316
+ return;
317
+ }
318
+ ctx.tui.showMessage("Approval state set to: approved");
319
+ },
320
+ });
321
+ // /reject — Mark the active runtime approval as rejected
322
+ registry.register({
323
+ name: "reject",
324
+ aliases: [],
325
+ description: "Reject the active runtime packet",
326
+ usage: "/reject",
327
+ handler: (_args, ctx) => {
328
+ if (!ctx.tui.setApprovalState("rejected")) {
329
+ ctx.tui.showMessage("No active runtime packet to reject.", "warn");
330
+ return;
331
+ }
332
+ ctx.tui.showMessage("Approval state set to: rejected");
333
+ },
334
+ });
335
+ // /trust — Keep the active runtime approved until revoked
336
+ registry.register({
337
+ name: "trust",
338
+ aliases: ["remember"],
339
+ description: "Trust the active runtime session until revoked",
340
+ usage: "/trust",
341
+ handler: (_args, ctx) => {
342
+ if (!ctx.tui.setApprovalState("approved")) {
343
+ ctx.tui.showMessage("No active runtime packet to trust.", "warn");
344
+ return;
345
+ }
346
+ ctx.tui.showMessage("Session trusted until /reject.");
347
+ },
348
+ });
307
349
  // /skill <name> — Load a skill
308
350
  registry.register({
309
351
  name: "skill",
@@ -386,6 +428,26 @@ export function registerBuiltinCommands(registry) {
386
428
  ctx.tui.createLogTab();
387
429
  },
388
430
  });
431
+ // /close — Close the current closeable tab
432
+ registry.register({
433
+ name: "close",
434
+ aliases: ["x"],
435
+ description: "Close the current tab",
436
+ usage: "/close",
437
+ handler: (_args, ctx) => {
438
+ ctx.tui.closeCurrentTab();
439
+ },
440
+ });
441
+ // /archive — Archive the current chat tab, then close it
442
+ registry.register({
443
+ name: "archive",
444
+ aliases: ["arc"],
445
+ description: "Archive the current chat tab and close it",
446
+ usage: "/archive",
447
+ handler: async (_args, ctx) => {
448
+ await ctx.tui.archiveCurrentTab();
449
+ },
450
+ });
389
451
  // /clear — Clear current view
390
452
  registry.register({
391
453
  name: "clear",
@@ -6,6 +6,9 @@
6
6
  */
7
7
  import { EventEmitter } from "node:events";
8
8
  import type { DashboardData } from "./layout.js";
9
+ import type { AceRuntimeStatusPacket } from "../store/repositories/local-model-runtime-repository.js";
10
+ export declare function isProcessAlive(pid: number): boolean;
11
+ export declare function selectLiveRuntimeStatus(statuses: Array<Pick<AceRuntimeStatusPacket, "process_id"> & AceRuntimeStatusPacket>): AceRuntimeStatusPacket | undefined;
9
12
  export declare class DashboardState extends EventEmitter {
10
13
  private workspaceRoot;
11
14
  private readonly storePath;
@@ -14,10 +17,12 @@ export declare class DashboardState extends EventEmitter {
14
17
  private persistedEvents;
15
18
  private runtimeEvents;
16
19
  private phase;
20
+ private latestRuntimeStatus;
17
21
  constructor(workspaceRoot: string);
18
22
  startWatching(): void;
19
23
  stopWatching(): void;
20
24
  getData(provider: string, model: string, startTime: number, tokensIn: number, tokensOut: number, tokensPerSec: number, activeAgents: number, totalAgents: number): DashboardData;
25
+ getLatestRuntimeStatus(): AceRuntimeStatusPacket | undefined;
21
26
  addEvent(agent: string, message: string): void;
22
27
  private applySnapshot;
23
28
  }
@@ -5,8 +5,8 @@
5
5
  * The TUI never tails compatibility files directly.
6
6
  */
7
7
  import { EventEmitter } from "node:events";
8
- import { resolve } from "node:path";
9
8
  import { pollStore } from "../store/state-reader.js";
9
+ import { getWorkspaceStorePath } from "../store/store-snapshot.js";
10
10
  function dedupeEvents(events) {
11
11
  const seen = new Set();
12
12
  const ordered = [];
@@ -19,7 +19,37 @@ function dedupeEvents(events) {
19
19
  }
20
20
  return ordered.slice(-500);
21
21
  }
22
+ export function isProcessAlive(pid) {
23
+ if (!Number.isInteger(pid) || pid <= 0)
24
+ return false;
25
+ try {
26
+ process.kill(pid, 0);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ export function selectLiveRuntimeStatus(statuses) {
34
+ return statuses.find((status) => isProcessAlive(status.process_id));
35
+ }
22
36
  function derivePhase(snapshot) {
37
+ const runtimeStatus = selectLiveRuntimeStatus(snapshot.runtimeStatuses);
38
+ if (runtimeStatus?.bridge_status === "blocked") {
39
+ return "Blocked";
40
+ }
41
+ if (runtimeStatus?.bridge_status === "approval_pending") {
42
+ return "Approval Pending";
43
+ }
44
+ if (runtimeStatus?.bridge_status === "needs_input") {
45
+ return "Needs Input";
46
+ }
47
+ if (runtimeStatus?.bridge_status === "retrying") {
48
+ return "Retrying";
49
+ }
50
+ if (runtimeStatus?.bridge_status === "running") {
51
+ return "Running";
52
+ }
23
53
  if (snapshot.jobs.some((job) => String(job.status ?? "") === "running")) {
24
54
  return "Running";
25
55
  }
@@ -42,10 +72,11 @@ export class DashboardState extends EventEmitter {
42
72
  persistedEvents = [];
43
73
  runtimeEvents = [];
44
74
  phase = "Initializing";
75
+ latestRuntimeStatus;
45
76
  constructor(workspaceRoot) {
46
77
  super();
47
78
  this.workspaceRoot = workspaceRoot;
48
- this.storePath = resolve(workspaceRoot, ".agents", "ACE", "ace-state.ace");
79
+ this.storePath = getWorkspaceStorePath(workspaceRoot);
49
80
  }
50
81
  startWatching() {
51
82
  if (this.stopPolling)
@@ -74,10 +105,14 @@ export class DashboardState extends EventEmitter {
74
105
  tokensIn,
75
106
  tokensOut,
76
107
  tokensPerSec,
108
+ runtimeStatus: this.latestRuntimeStatus,
77
109
  tasks: this.tasks,
78
110
  events: dedupeEvents([...this.persistedEvents, ...this.runtimeEvents]).slice(-50),
79
111
  };
80
112
  }
113
+ getLatestRuntimeStatus() {
114
+ return this.latestRuntimeStatus;
115
+ }
81
116
  addEvent(agent, message) {
82
117
  this.runtimeEvents.push({
83
118
  timestamp: Date.now(),
@@ -105,6 +140,7 @@ export class DashboardState extends EventEmitter {
105
140
  }));
106
141
  this.persistedEvents = dedupeEvents([...trackerEvents, ...ledgerEvents]).slice(-300);
107
142
  this.phase = derivePhase(snapshot);
143
+ this.latestRuntimeStatus = selectLiveRuntimeStatus(snapshot.runtimeStatuses);
108
144
  // Surface discovered providers to the TUI controller so dashboard controls
109
145
  // reflect what's actually available in the store.
110
146
  if (snapshot.providers.length > 0) {
@@ -30,6 +30,8 @@ export declare class AceTui implements TuiController {
30
30
  private messageQueue;
31
31
  private workspaceRoot;
32
32
  private providerBaseUrls;
33
+ private latestRuntimeStatus;
34
+ private pendingCloseTabId;
33
35
  constructor(options?: {
34
36
  provider?: string;
35
37
  model?: string;
@@ -64,6 +66,7 @@ export declare class AceTui implements TuiController {
64
66
  private syncTabState;
65
67
  /** 1-second tick for status bar, message expiry */
66
68
  private tick;
69
+ private getActiveRuntimeStatus;
67
70
  private normalizeProvider;
68
71
  static defaultSystemPrompt(): string;
69
72
  private ensureProvider;
@@ -92,9 +95,11 @@ export declare class AceTui implements TuiController {
92
95
  createChatTab(): void;
93
96
  createLogTab(): void;
94
97
  closeCurrentTab(): void;
98
+ archiveCurrentTab(): Promise<void>;
95
99
  showMessage(text: string, level?: "info" | "warn" | "error"): void;
96
100
  clearView(): void;
97
101
  refresh(): void;
102
+ setApprovalState(state: "not_required" | "pending" | "approved" | "rejected"): boolean;
98
103
  getStatus(): Record<string, unknown>;
99
104
  quit(): void;
100
105
  sendChatMessage(text: string): Promise<void>;