@yeaft/webchat-agent 1.0.613 → 1.0.616

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.
@@ -13,7 +13,8 @@ import { isTerminalAgentStatus } from '../sub-agent/status.js';
13
13
  import { describeAgentOutcome } from '../sub-agent/outcome.js';
14
14
  import { NullTrace } from '../debug-trace.js';
15
15
  import { loadConfig } from '../config.js';
16
- import { digest, fail, PERSON_TASK_LIMITS, personTaskRequest } from './contracts.js';
16
+ import { digest, fail, PERSON_TASK_LIMITS, personTaskRequest, taskInspectionCursor } from './contracts.js';
17
+ import { inspectionPage, inspectionTime } from './inspection.js';
17
18
 
18
19
  // Deliberately not the full Session registry: no transcript search, routing,
19
20
  // interactive UI, MCP, Work Center, or nested orchestration authority.
@@ -77,13 +78,13 @@ function projectedTask(task) {
77
78
  sourceEpisodeId: task.source?.episodeId ?? null, agentId: task.runtime?.subAgentId ?? null,
78
79
  recoveryStatus: task.status === 'orphaned' ? 'orphaned' : null };
79
80
  }
80
- function projectedAgent(agent, executionPending) {
81
+ function projectedAgent(agent, executionPending, taskId) {
81
82
  const usage = Object.fromEntries(['tokens', 'turns', 'startedAt'].map(key => [key,
82
83
  Number.isFinite(agent.usage?.[key]) && agent.usage[key] >= 0 ? agent.usage[key] : 0]));
83
84
  // Only the helper's fixed outcome vocabulary crosses this boundary, never the
84
85
  // native result, budget reason, final report, or partial output.
85
86
  const { status, complete, reason, truncated } = describeAgentOutcome(agent);
86
- return { id: agent.id, name: summary(agent.name, 160), status: agent.status,
87
+ return { id: agent.id, taskId, name: summary(agent.name, 160), status: agent.status,
87
88
  outcome: { status, complete, reason, truncated }, executionPending,
88
89
  mission: summary(agent.mission ?? agent.task, 1024), usage, createdAt: agent.createdAt ?? null,
89
90
  sourceEpisodeId: agent.personEpisodeId ?? null, recoveryStatus: agent.recoveryStatus === 'orphaned' ? 'orphaned' : null };
@@ -440,6 +441,17 @@ class ScopedPersonTaskHost {
440
441
  return task;
441
442
  }
442
443
 
444
+ #projectAgent(agent, executionPending) {
445
+ // The persistent link is independent of either history page. Only canonical
446
+ // IDs of this Person's actual child tasks may become owner log controls.
447
+ const id = agent.taskId;
448
+ const task = typeof id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(id)
449
+ ? this.taskManager.getTask(this.sessionId, id, this.parentVpId) : null;
450
+ const taskId = task && task.id === id && task.sessionId === this.sessionId && task.kind === 'sub_agent'
451
+ && task.runtime?.subAgentId === agent.id ? id : null;
452
+ return projectedAgent(agent, executionPending, taskId);
453
+ }
454
+
443
455
  /** Owner-facing inspection/control: never attach a provider, schedule cognition
444
456
  * or cancel unrelated work. Native snapshots remain private to evidence(). */
445
457
  async request({ ownerId, personId, namespace = this.scope.namespace, op, payload = {} } = {}) {
@@ -448,16 +460,46 @@ class ScopedPersonTaskHost {
448
460
  const args = personTaskRequest(op, payload);
449
461
  switch (op) {
450
462
  case 'tasks': {
463
+ const continuation = args.cursor ? taskInspectionCursor(args.cursor) : null;
464
+ if (continuation && continuation.scope !== this.scope.key) throw denied();
451
465
  this.#persistAgents();
452
466
  const tasks = jsonFiles(this.#manager.store.sessionDir(this.sessionId))
453
467
  .filter(task => task.sessionId === this.sessionId && task.ownerVpId === this.parentVpId && ['shell', 'sub_agent'].includes(task.kind));
454
468
  const agents = [...this.#agents.values()].filter(agent => agentBelongsToScope(agent, this.agentScope));
469
+ const byCreation = (a, b) => inspectionTime(b.createdAt) - inspectionTime(a.createdAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
470
+ if (args.limit !== undefined) {
471
+ const readPage = (items, key, view) => {
472
+ const boundary = continuation?.[key];
473
+ if (continuation && boundary === null) return { items: [], nextCursor: null };
474
+ const records = items.filter(item => !boundary || inspectionTime(item.createdAt) < boundary.time ||
475
+ (inspectionTime(item.createdAt) === boundary.time && item.id > boundary.id))
476
+ .sort(byCreation)
477
+ .slice(0, args.limit + 1);
478
+ return inspectionPage(records, args.limit, item => ({ time: inspectionTime(item.createdAt), id: item.id }), view);
479
+ };
480
+ const taskPage = readPage(tasks, 'tasks', projectedTask);
481
+ const agentPage = readPage(agents, 'agents', agent => this.#projectAgent(agent, agent.executionPending));
482
+ const more = taskPage.nextCursor !== null || agentPage.nextCursor !== null;
483
+ const nextCursor = more ? `t1:${Buffer.from(JSON.stringify({ scope: this.scope.key,
484
+ tasks: taskPage.nextCursor, agents: agentPage.nextCursor })).toString('base64url')}` : null;
485
+ // History stays strictly newest-created first. A separate, bounded
486
+ // control snapshot keeps older live shells and terminal children with
487
+ // detached tools reachable without scrolling through settled history.
488
+ const activePage = (items, isActive, view) => inspectionPage(items.filter(isActive).sort(byCreation),
489
+ PERSON_TASK_LIMITS.records, 'id', view);
490
+ const activeTasks = activePage(tasks, task => !isTerminalTaskStatus(task.status), projectedTask);
491
+ const activeAgents = activePage(agents, agent => !isTerminalAgentStatus(agent.status) || agent.executionPending,
492
+ agent => this.#projectAgent(agent, agent.executionPending));
493
+ return { tasks: taskPage.items, agents: agentPage.items, nextCursor, truncated: more,
494
+ active: { tasks: activeTasks.items, agents: activeAgents.items,
495
+ truncated: activeTasks.nextCursor !== null || activeAgents.nextCursor !== null } };
496
+ }
455
497
  const recent = (items, settled) => items.sort((a, b) => Number(settled(a)) - Number(settled(b))
456
- || String(b.updatedAt ?? b.createdAt ?? '').localeCompare(String(a.updatedAt ?? a.createdAt ?? ''))).slice(0, PERSON_TASK_LIMITS.records);
498
+ || byCreation(a, b)).slice(0, PERSON_TASK_LIMITS.records);
457
499
  // A terminal driver with actual tools still running is active work, not
458
500
  // disposable history: keep its cleanup control ahead of settled records.
459
501
  return { tasks: recent(tasks, task => isTerminalTaskStatus(task.status)).map(projectedTask),
460
- agents: recent(agents, agent => isTerminalAgentStatus(agent.status) && !agent.executionPending).map(agent => projectedAgent(agent, agent.executionPending)),
502
+ agents: recent(agents, agent => isTerminalAgentStatus(agent.status) && !agent.executionPending).map(agent => this.#projectAgent(agent, agent.executionPending)),
461
503
  truncated: tasks.length > PERSON_TASK_LIMITS.records || agents.length > PERSON_TASK_LIMITS.records };
462
504
  }
463
505
  case 'task_log': {
@@ -483,7 +525,7 @@ class ScopedPersonTaskHost {
483
525
  parentEngineDeps: this.agentScope, taskManager: this.taskManager }));
484
526
  if (!result.success) fail('TASK_CONTROL_UNAVAILABLE');
485
527
  await this.#joinAgent(agent);
486
- return { agent: projectedAgent(agent, this.#executionPending(agent)), pending: false };
528
+ return { agent: this.#projectAgent(agent, this.#executionPending(agent)), pending: false };
487
529
  }
488
530
  default: fail('INVALID_REQUEST');
489
531
  }