@yeaft/webchat-agent 1.0.612 → 1.0.615

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": "@yeaft/webchat-agent",
3
- "version": "1.0.612",
3
+ "version": "1.0.615",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -2,7 +2,7 @@ import { NATIVE_TOOL_MANIFESTS, NATIVE_TOOL_IDS, isNativeTool } from './native-t
2
2
  import { bytes, digest, fail, identifier, object, page, text } from './contracts.js';
3
3
  import { createdCapabilityRecord, validateCreatedDefinition } from './created-capability-contract.js';
4
4
  import { runPersonScript, scriptInput, testPersonScript } from './script-executor.js';
5
- import { createdSkillView, inspectionPage } from './inspection.js';
5
+ import { chronologicalCursor, createdSkillView, inspectionCursor, inspectionPage, inspectionTime } from './inspection.js';
6
6
 
7
7
  // Intrinsic cognition, bounded pure scripts and supported native host tools.
8
8
  // No shell/filesystem/network/VP authority is exposed to generated code.
@@ -83,10 +83,19 @@ export function inspectCapabilities(created, { cursor, limit }, nativeToolIds =
83
83
  contract.evidence = item.evidence;
84
84
  entries.push(item);
85
85
  }
86
- // IDs are ASCII identifiers. Binary ordering matches SQLite.
87
- const matches = entries.filter(entry => cursor === null || entry.id > cursor)
88
- .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
89
- return inspectionPage(matches, limit, 'id');
86
+ const boundary = inspectionCursor(cursor, 'skills');
87
+ // IDs are ASCII identifiers. Binary ordering matches SQLite. Only custom
88
+ // versions have creation dates; builtins follow as an undated ID catalogue.
89
+ const byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
90
+ if (boundary?.legacy) return inspectionPage(entries.filter(entry => entry.id > boundary.id).sort(byId), limit, 'id');
91
+ const custom = entries.filter(entry => entry.source.kind === 'person-created')
92
+ .filter(entry => !boundary || (boundary.stage === 'c' && (inspectionTime(entry.createdAt) < boundary.time ||
93
+ (inspectionTime(entry.createdAt) === boundary.time && entry.id > boundary.id))))
94
+ .sort((a, b) => inspectionTime(b.createdAt) - inspectionTime(a.createdAt) || byId(a, b));
95
+ const builtin = entries.filter(entry => entry.source.kind !== 'person-created')
96
+ .filter(entry => boundary?.stage !== 'b' || entry.id > boundary.id).sort(byId);
97
+ return inspectionPage([...custom, ...builtin], limit,
98
+ record => chronologicalCursor('skills', record, record.source.kind === 'person-created' ? 'c' : 'b'));
90
99
  }
91
100
 
92
101
  export class PersonCapabilities {
@@ -66,12 +66,39 @@ export function page(payload = {}) {
66
66
  }
67
67
  /** Task APIs are explicit owner actions, not cognitive admissions. Logs are raw
68
68
  * owner-scoped output; list/control responses never include native runtime/log/result.
69
- * Lists cap each collection at 100, preferring live records then recent history.
69
+ * Empty list payloads retain the legacy active-first capped inventory. Explicit
70
+ * {limit,cursor?} pages each collection newest-created first, with immutable
71
+ * creation-time/ID keysets and one scope-bound continuation for both collections.
72
+ * Paged responses also carry a bounded active control snapshot independent of
73
+ * history position (including terminal children with pending actual execution).
70
74
  * Log offsets count raw UTF-8 bytes; an omitted offset starts at the beginning.
71
75
  */
72
76
  export const PERSON_TASK_LIMITS = Object.freeze({ records: 100, logBytes: 16384, maxLogBytes: 65536 });
77
+ export function taskInspectionCursor(cursor) {
78
+ if (typeof cursor !== 'string' || !/^t1:[A-Za-z0-9_-]{1,2048}$/.test(cursor)) fail('INVALID_REQUEST');
79
+ let value;
80
+ try { value = JSON.parse(Buffer.from(cursor.slice(3), 'base64url').toString('utf8')); } catch { fail('INVALID_REQUEST'); }
81
+ object(value, ['scope', 'tasks', 'agents']);
82
+ if (typeof value.scope !== 'string' || !/^[a-f0-9]{64}$/.test(value.scope)) fail('INVALID_REQUEST');
83
+ for (const key of ['tasks', 'agents']) {
84
+ const boundary = value[key];
85
+ if (boundary === null) continue;
86
+ object(boundary, ['time', 'id']);
87
+ if (!Number.isSafeInteger(boundary.time) || boundary.time < 0 || typeof boundary.id !== 'string' ||
88
+ !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(boundary.id)) fail('INVALID_REQUEST');
89
+ }
90
+ if ((value.tasks === null && value.agents === null) || Buffer.from(JSON.stringify(value)).toString('base64url') !== cursor.slice(3)) fail('INVALID_REQUEST');
91
+ return value;
92
+ }
73
93
  export function personTaskRequest(op, payload = {}) {
74
- if (op === 'tasks') { object(payload, []); return {}; }
94
+ if (op === 'tasks') {
95
+ object(payload, ['cursor', 'limit'], []);
96
+ if (!Object.keys(payload).length) return {};
97
+ const limit = payload.limit === undefined ? PERSON_TASK_LIMITS.records : payload.limit, cursor = payload.cursor ?? null;
98
+ if (!Number.isInteger(limit) || limit < 1 || limit > PERSON_TASK_LIMITS.records) fail('INVALID_REQUEST');
99
+ if (cursor !== null) taskInspectionCursor(cursor);
100
+ return { cursor, limit };
101
+ }
75
102
  const idKey = op === 'agent_close' ? 'agentId' : 'taskId';
76
103
  object(payload, op === 'task_log' ? ['taskId', 'offset', 'maxBytes'] : [idKey], [idKey]);
77
104
  // Native TaskStore normalizes punctuation. Reject aliases instead of letting
@@ -1,12 +1,38 @@
1
1
  import { bytes, fail, identifier, object, page, text } from './contracts.js';
2
2
 
3
+ /** Timestamp + binary ID keysets survive insertion, deletion of the boundary
4
+ * record and same-millisecond writes. Builtins have no invented chronology.
5
+ * Bare ID cursors remain readable for pre-keyset clients' ID-ascending pages. */
6
+ export function inspectionCursor(value, section) {
7
+ if (value == null) return null;
8
+ if (typeof value !== 'string' || value.length > 512) fail('INVALID_REQUEST');
9
+ const prefix = section === 'memory' ? 'm1' : 's1';
10
+ const match = /^(m1|s1):(c|b):(\d{1,16}):([A-Za-z0-9_-]+)$/.exec(value);
11
+ if (!match) {
12
+ if (/^(m1|s1):/.test(value)) fail('INVALID_REQUEST');
13
+ identifier(value); return { legacy: true, id: value };
14
+ }
15
+ const [, type, stage, rawTime, encoded] = match;
16
+ const time = Number(rawTime), id = Buffer.from(encoded, 'base64url').toString('utf8');
17
+ identifier(id);
18
+ if (type !== prefix || (section === 'memory' && stage !== 'c') || !Number.isSafeInteger(time) || time < 0 ||
19
+ String(time) !== rawTime || (stage === 'b' && time !== 0) || Buffer.from(id).toString('base64url') !== encoded) fail('INVALID_REQUEST');
20
+ return { stage, time, id };
21
+ }
22
+ export const inspectionTime = value => {
23
+ const time = value == null ? NaN : new Date(value).getTime();
24
+ return Number.isSafeInteger(time) && time >= 0 ? time : 0;
25
+ };
26
+ export const chronologicalCursor = (section, record, stage = 'c') =>
27
+ `${section === 'memory' ? 'm1' : 's1'}:${stage}:${stage === 'b' ? 0 : inspectionTime(section === 'memory' ? record.updatedAt : record.createdAt)}:${Buffer.from(record.id).toString('base64url')}`;
28
+
3
29
  /** Public read APIs use keyset cursors, never offsets or model-ranked recall. */
4
30
  export function inspectRequest(payload = {}) {
5
31
  object(payload, ['section', 'cursor', 'limit'], ['section']);
6
32
  if (!['memory', 'skills'].includes(payload.section)) fail('INVALID_REQUEST');
7
33
  const { limit } = page({ limit: payload.limit });
8
34
  const cursor = payload.cursor ?? null;
9
- if (cursor !== null) identifier(cursor);
35
+ inspectionCursor(cursor, payload.section);
10
36
  return { section: payload.section, cursor, limit };
11
37
  }
12
38
  export function searchRequest(payload = {}) {
@@ -48,13 +74,13 @@ export function createdSkillView(record, contract) {
48
74
  /** Complete records only. Continuation follows the last returned key even when the
49
75
  * byte budget, rather than the requested count, ends a page. No attachment bytes. */
50
76
  export function inspectionPage(records, limit, key, view = value => value) {
51
- const items = []; let size = 0;
77
+ const items = []; let size = 0, lastRecord;
52
78
  for (const record of records) {
53
79
  if (items.length === limit) break;
54
80
  const item = view(record), cost = bytes(item);
55
81
  if (cost > 256 * 1024) fail('OUTPUT_LIMIT');
56
82
  if (items.length && size + cost > 256 * 1024) break;
57
- items.push(item); size += cost;
83
+ items.push(item); size += cost; lastRecord = record;
58
84
  }
59
- return { items, nextCursor: records.length > items.length ? String(items.at(-1)[key]) : null };
85
+ return { items, nextCursor: records.length > items.length ? (typeof key === 'function' ? key(lastRecord) : String(items.at(-1)[key])) : null };
60
86
  }
@@ -8,7 +8,7 @@ import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapa
8
8
  import { SCHEMA, TABLES } from './sqlite-schema.js';
9
9
  import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
10
10
  import { capabilityExperienceView, recordCapabilityExperience } from './capability-experience.js';
11
- import { conceptView, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
11
+ import { chronologicalCursor, conceptView, inspectionCursor, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
12
12
  import { TOKEN_FIELDS, turnsPage, turnView } from './turn-diagnostics.js';
13
13
  import { inspectCapabilities } from './capabilities.js';
14
14
 
@@ -476,8 +476,15 @@ export class SqlitePersonStore {
476
476
  this.getPerson(ownerId);
477
477
  const scope = this.scope(ownerId);
478
478
  if (section === 'skills') return inspectCapabilities(this.rows('created_capabilities', scope, ' ORDER BY id ASC LIMIT 32'), { cursor, limit }, nativeToolIds);
479
- const records = this.rows('concepts', scope, ' AND (? IS NULL OR id > ?) ORDER BY id ASC LIMIT ?', [cursor, cursor, limit + 1]);
480
- return inspectionPage(records, limit, 'id', conceptView);
479
+ const boundary = inspectionCursor(cursor, section);
480
+ if (boundary?.legacy) {
481
+ const records = this.rows('concepts', scope, ' AND id > ? ORDER BY id ASC LIMIT ?', [boundary.id, limit + 1]);
482
+ return inspectionPage(records, limit, 'id', conceptView);
483
+ }
484
+ const records = this.rows('concepts', scope,
485
+ ' AND (? IS NULL OR updatedAt < ? OR (updatedAt = ? AND id > ?)) ORDER BY updatedAt DESC, id ASC LIMIT ?',
486
+ [boundary?.time ?? null, boundary?.time ?? null, boundary?.time ?? null, boundary?.id ?? null, limit + 1]);
487
+ return inspectionPage(records, limit, record => chronologicalCursor(section, record), conceptView);
481
488
  }
482
489
  search(ownerId, options) {
483
490
  const { query, cursor, limit } = searchRequest(options);
@@ -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
  }