@yeaft/webchat-agent 1.0.610 → 1.0.612
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +133 -116
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/person/contracts.js +4 -1
- package/yeaft/person/inspection.js +1 -1
- package/yeaft/person/runtime.js +27 -7
- package/yeaft/person/service.js +3 -3
- package/yeaft/person/sqlite-repository.js +1 -0
- package/yeaft/person/sqlite-store.js +42 -4
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
// 只允许应用层显式产物;不要求或保存 provider 的隐藏推理。
|
|
4
|
-
export const LIMITS = Object.freeze({ inputBytes: 8192, outputBytes: 65536, contextBytes: 65536, calls: 16,
|
|
4
|
+
export const LIMITS = Object.freeze({ inputBytes: 8192, outputBytes: 65536, contextBytes: 65536, calls: 16, leaseMs: 15000 });
|
|
5
|
+
// Feedback cadence is not an execution deadline. Slow providers/tools remain cancellable.
|
|
6
|
+
export const FEEDBACK = Object.freeze({ minMs: 30000, maxMs: 60000 });
|
|
7
|
+
export const FEEDBACK_INSTRUCTIONS = "feedback describes user-visible communication, not a deadline. After about 30 seconds without a substantive reply, use reply for a concise, useful progress update at a natural execution boundary; aim for 30\u201360 seconds according to actual progress. Explain verified findings, limitations and what you are checking next, in the user's language. Do not repeat feedback.lastReply, invent progress, expose internal deliberation/decision/selfCheck, or claim an unexecuted tool succeeded. If no meaningful update exists, reply:null is appropriate; the host shows an honest waiting status around 60 seconds without interrupting the current request. With next non-null, a validated reply may be published as progress independently of cognitive state, and retained on cancellation/failure. Only next:null commits cognition and supplies the final reply. Do not call a tool or an extra model just to report progress. Each final reply should stand on its own and note unfinished work.";
|
|
5
8
|
const ERRORS = {
|
|
6
9
|
INVALID_REQUEST: 'Invalid digital person request.', NOT_CONFIGURED: 'Digital person storage configuration is missing.',
|
|
7
10
|
STORAGE_UNAVAILABLE: 'Digital person storage is unavailable. Check instance storage and database configuration.',
|
|
@@ -20,7 +20,7 @@ export function personName(value) {
|
|
|
20
20
|
const pick = (value, keys) => value == null ? null : Object.fromEntries(keys.filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
|
|
21
21
|
export const settingsView = value => pick(value, ['autonomyEnabled', 'modelCandidates', 'defaultModel']);
|
|
22
22
|
export function messageView(value) {
|
|
23
|
-
const result = pick(value, ['id', 'schemaVersion', 'revision', 'seq', 'episodeId', 'clientMessageId', 'role', 'text', 'createdAt']);
|
|
23
|
+
const result = pick(value, ['id', 'schemaVersion', 'revision', 'seq', 'episodeId', 'clientMessageId', 'callId', 'replyKind', 'role', 'text', 'createdAt']);
|
|
24
24
|
if (result && Array.isArray(value.attachments)) result.attachments = value.attachments.slice(0, 4)
|
|
25
25
|
.map(file => pick(file, ['id', 'name', 'mimeType', 'size', 'sha256', 'kind']));
|
|
26
26
|
return result;
|
package/yeaft/person/runtime.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { createPersonToolHost, createPersonNativeRegistry, isNativeTool, projectNativeResult } from './native-tools.js';
|
|
2
2
|
import { PersonTaskHost } from './task-host.js';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { utf8PrefixWithinBytes } from '../utf8.js';
|
|
4
5
|
import { CAPABILITY_MAP, CAPABILITY_LIMITS, foundationCapabilities, PersonCapabilities, catalogRevision as capabilityCatalogRevision } from './capabilities.js';
|
|
5
6
|
import { attachmentMetadata } from './attachments.js';
|
|
6
7
|
import { abortable, collectOutput } from './provider.js';
|
|
7
|
-
import { bytes, digest, fail, LIMITS, PersonError, PROPOSAL_INSTRUCTIONS, reportedLineage, safeError, validateProposal, validateSelection } from './contracts.js';
|
|
8
|
+
import { bytes, digest, fail, FEEDBACK, FEEDBACK_INSTRUCTIONS, LIMITS, PersonError, PROPOSAL_INSTRUCTIONS, reportedLineage, safeError, validateProposal, validateSelection } from './contracts.js';
|
|
8
9
|
|
|
9
10
|
const messageRef = m => `message:${m.id}:${m.revision}`;
|
|
10
11
|
const conceptRef = c => `concept:${c.id}:${c.revision}`;
|
|
11
12
|
|
|
12
13
|
/** Assemble bounded request copies. Omitting a record never deletes or truncates its durable original. */
|
|
13
|
-
export function assembleContext({ snapshot, episode, provider, selection, previous, capabilityResult, remainingCalls, taskEvidence, dependencyRefs = [], activeCapabilities = foundationCapabilities(), capabilityMap = CAPABILITY_MAP, attachments = [], environment }) {
|
|
14
|
+
export function assembleContext({ snapshot, episode, provider, selection, previous, capabilityResult, remainingCalls, taskEvidence, dependencyRefs = [], activeCapabilities = foundationCapabilities(), capabilityMap = CAPABILITY_MAP, attachments = [], environment, feedback }) {
|
|
14
15
|
const model = validateSelection(selection, provider.catalog);
|
|
15
16
|
// UTF-8 bytes is a conservative text-token bound; reserve explicit envelope/output overhead.
|
|
16
17
|
const images = attachments.filter(file => file.kind === 'image');
|
|
@@ -74,6 +75,16 @@ export function assembleContext({ snapshot, episode, provider, selection, previo
|
|
|
74
75
|
omittedCapabilities.push({ id: contract.id, reason: 'context-budget', inspect: 'catalog.view' });
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
// Feedback is a request-only hint, never grounds to fail the next call.
|
|
79
|
+
// Keep durable replies intact; avoid duplicating up to 8 KiB from previousProposal.
|
|
80
|
+
if (feedback) {
|
|
81
|
+
const preview = utf8PrefixWithinBytes(feedback.lastReply ?? '', 512).text;
|
|
82
|
+
context.feedback = { ...feedback, instructions: FEEDBACK_INSTRUCTIONS, lastReply: preview || null,
|
|
83
|
+
lastReplyTruncated: bytes(feedback.lastReply ?? '') > bytes(preview) };
|
|
84
|
+
if (!fits()) { context.feedback.lastReply = null; context.feedback.lastReplyTruncated = Boolean(feedback.lastReply); }
|
|
85
|
+
if (!fits()) delete context.feedback.instructions;
|
|
86
|
+
if (!fits()) delete context.feedback;
|
|
87
|
+
}
|
|
77
88
|
const add = (field, item, ref) => {
|
|
78
89
|
if (renderedRefs.has(ref)) return;
|
|
79
90
|
const inherited = sourceRefs.has(ref);
|
|
@@ -189,8 +200,8 @@ export class PersonRuntime {
|
|
|
189
200
|
isOwnerRunning(ownerId) { return this.pendingCancellations.has(ownerId) || [...this.running.values()].some(job => job.episode.ownerId === ownerId); }
|
|
190
201
|
async run(episode, controller) {
|
|
191
202
|
const { signal } = controller;
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
let lastReplyAt = new Date(episode.createdAt).getTime() || Date.now();
|
|
204
|
+
let lastReply = null;
|
|
194
205
|
let heartbeatRunning = false;
|
|
195
206
|
const heartbeat = setInterval(async () => {
|
|
196
207
|
if (heartbeatRunning || signal.aborted) return;
|
|
@@ -198,7 +209,7 @@ export class PersonRuntime {
|
|
|
198
209
|
try { await this.repository.heartbeat(episode); }
|
|
199
210
|
catch (error) { controller.abort(safeError(error)); }
|
|
200
211
|
finally { heartbeatRunning = false; }
|
|
201
|
-
}, Math.max(100, Math.floor(this.repository.leaseMs / 3)));
|
|
212
|
+
}, Math.max(100, Math.min(5000, Math.floor(this.repository.leaseMs / 3))));
|
|
202
213
|
heartbeat.unref?.();
|
|
203
214
|
try {
|
|
204
215
|
const provider = await abortable(this.getProvider(episode.modelCandidates ?? [], episode.defaultModel ?? null), signal);
|
|
@@ -232,7 +243,11 @@ export class PersonRuntime {
|
|
|
232
243
|
for (let index = 0; index < episode.budget.calls; index++) {
|
|
233
244
|
signal.throwIfAborted();
|
|
234
245
|
const callId = randomUUID();
|
|
235
|
-
const context = assembleContext({ snapshot, episode, provider, selection, previous, capabilityResult, dependencyRefs, remainingCalls: episode.budget.calls - index, taskEvidence: projectTaskEvidence(this.tasks()?.snapshot(episode.ownerId)), attachments, activeCapabilities: capabilities.context(), capabilityMap: capabilities.catalog(), environment: toolHost.environment
|
|
246
|
+
const context = assembleContext({ snapshot, episode, provider, selection, previous, capabilityResult, dependencyRefs, remainingCalls: episode.budget.calls - index, taskEvidence: projectTaskEvidence(this.tasks()?.snapshot(episode.ownerId)), attachments, activeCapabilities: capabilities.context(), capabilityMap: capabilities.catalog(), environment: toolHost.environment, feedback: {
|
|
247
|
+
minIntervalMs: FEEDBACK.minMs, maxIntervalMs: FEEDBACK.maxMs,
|
|
248
|
+
elapsedSinceReplyMs: Math.max(0, Date.now() - lastReplyAt),
|
|
249
|
+
due: Date.now() - lastReplyAt >= FEEDBACK.minMs, lastReply,
|
|
250
|
+
} });
|
|
236
251
|
capabilities.activate(context.activeCapabilities);
|
|
237
252
|
dependencyRefs = context.manifest.inputDependencyRefs;
|
|
238
253
|
for (const [id, concept] of context.concepts) readConcepts.set(id, concept);
|
|
@@ -289,6 +304,11 @@ export class PersonRuntime {
|
|
|
289
304
|
await this.repository.commit(episode, proposal, selection, callId, new Map(proposal.concepts.map(c => [c.id, c.epistemicState === 'reported' ? reportedLineage(c, readSources) : []])));
|
|
290
305
|
return;
|
|
291
306
|
}
|
|
307
|
+
if (proposal.reply) {
|
|
308
|
+
signal.throwIfAborted();
|
|
309
|
+
const published = await this.repository.publishProgress(episode, { callId, reply: proposal.reply });
|
|
310
|
+
if (published) { lastReply = published.text; lastReplyAt = new Date(published.createdAt).getTime(); }
|
|
311
|
+
}
|
|
292
312
|
if (index + 1 >= episode.budget.calls) {
|
|
293
313
|
await this.tasks()?.cancel({ ownerId: episode.ownerId, episodeId: episode.id, reason: 'CALL_BUDGET' });
|
|
294
314
|
await this.repository.finish(episode, 'budget_exhausted', 'CALL_BUDGET');
|
|
@@ -345,7 +365,7 @@ export class PersonRuntime {
|
|
|
345
365
|
// On authority failure no alternate memory store is used. Expired leases become durable interrupted records on next access.
|
|
346
366
|
await this.repository.finish(episode, status, safe.code).catch(() => {});
|
|
347
367
|
} finally {
|
|
348
|
-
|
|
368
|
+
clearInterval(heartbeat);
|
|
349
369
|
if (!signal.aborted) controller.abort(new PersonError('INTERRUPTED'));
|
|
350
370
|
}
|
|
351
371
|
}
|
package/yeaft/person/service.js
CHANGED
|
@@ -22,9 +22,9 @@ export function createPersonService(options = {}) {
|
|
|
22
22
|
identifier(namespace);
|
|
23
23
|
const configured = typeof yeaftDir === 'string' && Boolean(yeaftDir.trim());
|
|
24
24
|
const calls = options.maxCalls ?? LIMITS.calls;
|
|
25
|
-
|
|
25
|
+
// Legacy timeoutMs is intentionally ignored: an episode has no wall-clock deadline.
|
|
26
26
|
const leaseMs = options.leaseMs ?? LIMITS.leaseMs;
|
|
27
|
-
if (!Number.isInteger(calls) || calls < 1 || calls > 32 ||
|
|
27
|
+
if (!Number.isInteger(calls) || calls < 1 || calls > 32 ||
|
|
28
28
|
!Number.isInteger(leaseMs) || leaseMs < 300 || leaseMs > 60000) fail('INVALID_REQUEST');
|
|
29
29
|
if (allowedModels != null && (!Array.isArray(allowedModels) || !allowedModels.length || allowedModels.some(m => typeof m !== 'string'))) fail('INVALID_REQUEST');
|
|
30
30
|
const repository = configured ? new SqlitePersonRepository({ yeaftDir, namespace, leaseMs }) : null;
|
|
@@ -43,7 +43,7 @@ export function createPersonService(options = {}) {
|
|
|
43
43
|
? memory.recall(ownerId, args, { signal }) : literalRecall(ownerId, args);
|
|
44
44
|
// Config/adapter are loaded per explicit episode, not a permanent stale cache.
|
|
45
45
|
const getProvider = (modelCandidates = [], defaultModel = null) => createPersonProvider({ yeaftDir, config, adapter, allowedModels, modelCandidates, defaultModel, effortEnabled: options.effortEnabled });
|
|
46
|
-
const runtime = new PersonRuntime({ repository, getProvider, budget: { calls
|
|
46
|
+
const runtime = new PersonRuntime({ repository, getProvider, budget: { calls }, workDir, yeaftDir, config, namespace });
|
|
47
47
|
let closed = false;
|
|
48
48
|
const requests = new Set();
|
|
49
49
|
async function handle({ ownerId, op, payload = {} } = {}) {
|
|
@@ -100,6 +100,7 @@ export class SqlitePersonRepository {
|
|
|
100
100
|
async episodeAttachments(episode) { return this.request('episodeAttachments', [episode]); }
|
|
101
101
|
async context(episode) { return this.request('context', [episode]); }
|
|
102
102
|
async recall(ownerId, options) { return this.request('recall', [ownerId, options]); }
|
|
103
|
+
async publishProgress(episode, data) { return this.request('publishProgress', [episode, data]); }
|
|
103
104
|
async commit(episode, proposal, selection, callId, reportedSources = new Map()) { return this.request('commit', [episode, proposal, selection, callId, reportedSources]); }
|
|
104
105
|
async finish(episode, status, code) { return this.request('finish', [episode, status, code]); }
|
|
105
106
|
async cancel(ownerId, episodeId) { return this.request('cancel', [ownerId, episodeId]); }
|
|
@@ -3,7 +3,7 @@ import { mkdirSync } from 'node:fs';
|
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { isMainThread } from 'node:worker_threads';
|
|
6
|
-
import { admissionReceipt, bytes, digest, fail, identifier, LIMITS, PersonError, text } from './contracts.js';
|
|
6
|
+
import { admissionReceipt, bytes, digest, fail, FEEDBACK, identifier, LIMITS, PersonError, text } from './contracts.js';
|
|
7
7
|
import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapability } from './created-capability-contract.js';
|
|
8
8
|
import { SCHEMA, TABLES } from './sqlite-schema.js';
|
|
9
9
|
import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
|
|
@@ -53,7 +53,7 @@ const publicOutput = (output, failed) => {
|
|
|
53
53
|
: { text: output.text, bytes: retainedBytes, complete: true, usage, stopReason };
|
|
54
54
|
};
|
|
55
55
|
const READS = new Set(['receipt', 'turns', 'inspect', 'search', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
|
|
56
|
-
const WRITES = new Set(['open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'startCapability', 'finalizeCapability', 'commit', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
|
|
56
|
+
const WRITES = new Set(['open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'startCapability', 'finalizeCapability', 'commit', 'publishProgress', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
|
|
57
57
|
const memoryKind = kind => { if (!['messages', 'concepts'].includes(kind)) fail('INVALID_REQUEST'); return kind; };
|
|
58
58
|
const boundedLimit = (limit, max = 100) => { if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) fail('INVALID_REQUEST'); return limit; };
|
|
59
59
|
const sequence = value => {
|
|
@@ -257,6 +257,17 @@ export class SqlitePersonStore {
|
|
|
257
257
|
own(episode) { const p = this.fenced(episode); if (!p) fail('STALE'); return p; }
|
|
258
258
|
heartbeat(episode) {
|
|
259
259
|
const p = this.own(episode); p.leaseUntil = new Date(this.now.getTime() + this.leaseMs); p.writeSerial++; this.put('persons', p);
|
|
260
|
+
const record = this.one('episodes', this.episodeScope(episode), ' AND id = ?', [episode.id]);
|
|
261
|
+
if (!record || record.status !== 'running') fail('STALE');
|
|
262
|
+
const lastAt = new Date(record.feedback?.at ?? record.lastReplyAt ?? record.createdAt).getTime();
|
|
263
|
+
// Heartbeats are at most 5s apart: publish the fallback between 55–60s.
|
|
264
|
+
// This updates one snapshot field, never adds messages or schedules cognition.
|
|
265
|
+
if (this.now.getTime() - lastAt >= FEEDBACK.maxMs - 5000 || record.feedback) {
|
|
266
|
+
const phase = record.openCall ? 'model' : record.openCapability ? 'capability' : 'preparing';
|
|
267
|
+
record.feedback = { at: this.now.getTime() - lastAt >= FEEDBACK.maxMs - 5000 ? this.now.toISOString() : record.feedback.at, phase };
|
|
268
|
+
if (phase === 'capability') record.feedback.capabilityId = record.openCapability.capability?.id;
|
|
269
|
+
this.put('episodes', record);
|
|
270
|
+
}
|
|
260
271
|
}
|
|
261
272
|
append(episode, kind, data) {
|
|
262
273
|
// Call proof/publication events are emitted only by their transactional methods.
|
|
@@ -366,6 +377,32 @@ export class SqlitePersonStore {
|
|
|
366
377
|
state: stateView(state), messages: this.rows('messages', scope, ' ORDER BY seq DESC LIMIT 12').reverse().map(messageView),
|
|
367
378
|
concepts: [...new Map([...focused, ...recent].map(c => [c.id, conceptView(c)])).values()] };
|
|
368
379
|
}
|
|
380
|
+
/** Publish only a validated, complete call's reply. This is communication, not
|
|
381
|
+
* a state/Concept commit. Fencing, dedupe and the memory journal share one transaction. */
|
|
382
|
+
publishProgress(episode, { callId, reply }) {
|
|
383
|
+
const p = this.own(episode), scope = this.episodeScope(episode);
|
|
384
|
+
identifier(callId); text(reply, 8192);
|
|
385
|
+
const record = this.one('episodes', scope, ' AND id = ?', [episode.id]);
|
|
386
|
+
if (!record || record.status !== 'running' || record.openCall || record.openCapability) fail('STALE');
|
|
387
|
+
const prior = this.rows('messages', scope, " AND json_extract(record, '$.episodeId') = ? AND json_extract(record, '$.replyKind') = 'progress'", [episode.id]);
|
|
388
|
+
const duplicate = prior.find(m => m.callId === callId);
|
|
389
|
+
if (duplicate) { if (duplicate.text !== reply) fail('INVALID_REQUEST'); return messageView(duplicate); }
|
|
390
|
+
const proof = this.one('traces', scope, " AND json_extract(record, '$.episodeId') = ? AND json_extract(record, '$.kind') = 'call_output' AND json_extract(record, '$.callId') = ? AND json_extract(record, '$.output.complete') = 1", [episode.id, callId]);
|
|
391
|
+
const accepted = this.one('traces', scope, " AND json_extract(record, '$.episodeId') = ? AND json_extract(record, '$.kind') = 'activity' AND json_extract(record, '$.callId') = ? AND json_extract(record, '$.disposition') = 'candidate'", [episode.id, callId]);
|
|
392
|
+
let proposal;
|
|
393
|
+
try { proposal = JSON.parse(proof?.output?.text); } catch { fail('INVALID_REQUEST'); }
|
|
394
|
+
if (!accepted || !proposal.next || proposal.reply !== reply) fail('INVALID_REQUEST');
|
|
395
|
+
// Provider latency counts too: a call started before 30s can finish after it.
|
|
396
|
+
if (this.now.getTime() - new Date(record.lastReplyAt ?? record.createdAt).getTime() < FEEDBACK.minMs ||
|
|
397
|
+
prior.some(m => m.text.trim() === reply.trim())) return null;
|
|
398
|
+
p.messageSeq++; p.writeSerial++; this.put('persons', p);
|
|
399
|
+
const message = this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id,
|
|
400
|
+
callId, replyKind: 'progress', role: 'assistant', text: reply, createdAt: this.now });
|
|
401
|
+
this.put('messages', message, true); this.journal(scope, 'messages', message);
|
|
402
|
+
record.lastReplyAt = this.now.toISOString(); delete record.feedback; this.put('episodes', record);
|
|
403
|
+
this.trace(episode.ownerId, episode.id, 'progress_reply', { callId, messageId: message.id });
|
|
404
|
+
return messageView(message);
|
|
405
|
+
}
|
|
369
406
|
commit(episode, proposal, selection, callId, reportedSources = new Map()) {
|
|
370
407
|
const p = this.own(episode), scope = this.episodeScope(episode);
|
|
371
408
|
if (proposal.baseStateVersion !== episode.baseStateVersion) fail('STALE');
|
|
@@ -391,7 +428,7 @@ export class SqlitePersonStore {
|
|
|
391
428
|
this.put('state_commits', this.doc(scope, { id: randomUUID(), version: p.stateVersion, parentVersion: episode.baseStateVersion,
|
|
392
429
|
episodeId: episode.id, callId, conceptRevisions: revisions, state: stateView(state), createdAt: this.now }), true);
|
|
393
430
|
if (proposal.reply) {
|
|
394
|
-
const message = this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id, role: 'assistant', text: proposal.reply, createdAt: this.now });
|
|
431
|
+
const message = this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id, callId, replyKind: 'final', role: 'assistant', text: proposal.reply, createdAt: this.now });
|
|
395
432
|
this.put('messages', message, true); this.journal(scope, 'messages', message);
|
|
396
433
|
}
|
|
397
434
|
const record = this.one('episodes', scope, ' AND id = ?', [episode.id]);
|
|
@@ -504,7 +541,8 @@ export class SqlitePersonStore {
|
|
|
504
541
|
const scope = this.scope(ownerId), p = this.getPerson(ownerId), state = this.one('states', scope);
|
|
505
542
|
const messages = this.rows('messages', scope, ' ORDER BY seq DESC LIMIT 21');
|
|
506
543
|
const episode = this.one('episodes', scope, ' ORDER BY inputWatermark DESC');
|
|
507
|
-
const latestEpisode = episode ? { id: episode.id, status: episode.status,
|
|
544
|
+
const latestEpisode = episode ? { id: episode.id, status: episode.status, createdAt: episode.createdAt,
|
|
545
|
+
...(episode.status === 'running' && episode.feedback ? { feedback: episode.feedback } : {}),
|
|
508
546
|
...(episode.terminalCode ? { terminalCode: episode.terminalCode } : {}), ...(episode.endedAt ? { endedAt: episode.endedAt } : {}) } : null;
|
|
509
547
|
return { latestEpisode, person: this.personView(p), state: stateView(state), concepts: this.focused(scope, state.focusConceptIds).slice(0, 12).map(conceptView),
|
|
510
548
|
messages: messages.slice(0, 20).reverse().map(messageView), nextMessagesCursor: messages.length > 20 ? String(messages[19].seq) : null,
|