@yeaft/webchat-agent 1.0.605 → 1.0.607
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/server/handlers/client-person.js +15 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +158 -96
- 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/bridge.js +3 -0
- package/yeaft/person/contracts.js +24 -1
- package/yeaft/person/inspection.js +1 -1
- package/yeaft/person/provider.js +29 -13
- package/yeaft/person/repository.js +4 -3
- package/yeaft/person/runtime.js +2 -2
- package/yeaft/person/service.js +53 -25
- package/yeaft/person/sqlite-repository.js +1 -1
- package/yeaft/person/sqlite-store.js +4 -3
- package/yeaft/person/task-host.js +124 -9
|
Binary file
|
package/package.json
CHANGED
package/yeaft/person/bridge.js
CHANGED
|
@@ -73,6 +73,9 @@ export function createPersonBridge({
|
|
|
73
73
|
IMAGE_MODEL: 'No permitted selected model supports image input',
|
|
74
74
|
MODEL_SELECTION: 'Choose models from this Agent configured catalog',
|
|
75
75
|
NOT_OPEN: 'Open the digital person first',
|
|
76
|
+
NOT_FOUND: 'Digital person task or child not found',
|
|
77
|
+
TASK_SCOPE_DENIED: 'Digital person task access denied',
|
|
78
|
+
TASK_CONTROL_UNAVAILABLE: 'Task control could not be confirmed; refresh before retrying',
|
|
76
79
|
STALE: 'Digital person state changed; refresh and try again',
|
|
77
80
|
IDEMPOTENCY_CONFLICT: 'This message identifier already belongs to a different request',
|
|
78
81
|
STORAGE_MISMATCH: 'The configured storage differs from the existing Person authority; do not switch without a verified migration',
|
|
@@ -20,6 +20,9 @@ const ERRORS = {
|
|
|
20
20
|
ATTACHMENT_LIMIT: 'Attachments exceed the limit: 4 files, 5 MiB each, 10 MiB total; text plus extracted content must fit 24 KiB.',
|
|
21
21
|
IMAGE_MODEL: 'The selected model does not permit image input.',
|
|
22
22
|
CLOSED: 'The digital person service is closed.',
|
|
23
|
+
NOT_FOUND: 'Digital person task or child not found.',
|
|
24
|
+
TASK_SCOPE_DENIED: 'Digital person task access denied.',
|
|
25
|
+
TASK_CONTROL_UNAVAILABLE: 'Task control could not be confirmed; refresh before retrying.',
|
|
23
26
|
};
|
|
24
27
|
export class PersonError extends Error {
|
|
25
28
|
constructor(code) { super(ERRORS[code] || ERRORS.INVALID_REQUEST); this.name = 'PersonError'; this.code = code in ERRORS ? code : 'INVALID_REQUEST'; }
|
|
@@ -58,6 +61,26 @@ export function page(payload = {}) {
|
|
|
58
61
|
}
|
|
59
62
|
return { cursor, limit };
|
|
60
63
|
}
|
|
64
|
+
/** Task APIs are explicit owner actions, not cognitive admissions. Logs are raw
|
|
65
|
+
* owner-scoped output; list/control responses never include native runtime/log/result.
|
|
66
|
+
* Lists cap each collection at 100, preferring live records then recent history.
|
|
67
|
+
* Log offsets count raw UTF-8 bytes; an omitted offset starts at the beginning.
|
|
68
|
+
*/
|
|
69
|
+
export const PERSON_TASK_LIMITS = Object.freeze({ records: 100, logBytes: 16384, maxLogBytes: 65536 });
|
|
70
|
+
export function personTaskRequest(op, payload = {}) {
|
|
71
|
+
if (op === 'tasks') { object(payload, []); return {}; }
|
|
72
|
+
const idKey = op === 'agent_close' ? 'agentId' : 'taskId';
|
|
73
|
+
object(payload, op === 'task_log' ? ['taskId', 'offset', 'maxBytes'] : [idKey], [idKey]);
|
|
74
|
+
// Native TaskStore normalizes punctuation. Reject aliases instead of letting
|
|
75
|
+
// two browser IDs resolve to the same persisted task.
|
|
76
|
+
if (typeof payload[idKey] !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(payload[idKey])) fail('INVALID_REQUEST');
|
|
77
|
+
if (op !== 'task_log') return { [idKey]: payload[idKey] };
|
|
78
|
+
const offset = payload.offset === undefined ? 0 : payload.offset;
|
|
79
|
+
const maxBytes = payload.maxBytes === undefined ? PERSON_TASK_LIMITS.logBytes : payload.maxBytes;
|
|
80
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > PERSON_TASK_LIMITS.maxLogBytes) fail('INVALID_REQUEST');
|
|
81
|
+
return { taskId: payload.taskId, offset, maxBytes };
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
const kinds = ['claim', 'question', 'method', 'self-model', 'scenario', 'interest'];
|
|
62
85
|
const epistemics = ['reported', 'hypothesis', 'imagined', 'uncertain'];
|
|
63
86
|
const activities = ['think', 'recall', 'reorganize', 'associate', 'rethink', 'imagine', 'respond', 'rest'];
|
|
@@ -130,4 +153,4 @@ Use the actual numeric baseStateVersion. New concept IDs use simple stable ident
|
|
|
130
153
|
budget.remainingCalls includes this call. Reserve the final call for a proposal with next:null; An active Recall followed by a final proposal needs two calls total; search (with a returned contract), capability use and final proposal need three. Do not search or inspect an already active capability merely to follow stages. If evidence remains insufficient at the final call, record the uncertainty or rest instead of inventing a result. No additional call is granted after the budget ends.
|
|
131
154
|
capabilities.active contains complete current contracts in foundation, familiar (actual execution observations), and discovered layers. Use any active capability directly; availability is not permission or a duty. Familiarity does not prove truth/usefulness; evaluate results in decision.selfCheck, ignore unhelpful habits, or rest. If no active ability fits, search or view a known catalog ID. Search prepares returned contracts; omitted contracts require view. Only rendered current contracts execute. No separate selector model or hidden cognition is involved.
|
|
132
155
|
Capability.create tests pure JSON transformations in restricted QuickJS and publishes only passing versions to this Person's private catalog. Tests do not prove correctness/usefulness; publication is durable even if this episode fails. Follow its complete active contract. Script.* cannot access host tools, files, shell, network or credentials, grant permissions, install packages or run automatically. Search before duplicating existing capabilities; avoid embedding private data or one-off constants.
|
|
133
|
-
Think is intrinsic: recall, reorganize, associate, reconsider, imagine or rest.
|
|
156
|
+
Think is intrinsic: recall, reorganize, associate, reconsider, imagine or rest. Own ongoing authorized work using task/child context, not only latest user text. Episode end is not task completion/cancellation; evidence never enables timers, autonomy or model re-entry. You remain one Person, not a task coordinator. Only next:null commits; intermediate proposals do not. Dream is idle imagination, not a user message or mandatory useful work; reply may be null.`;
|
|
@@ -18,7 +18,7 @@ export function personName(value) {
|
|
|
18
18
|
return text(value.trim(), 160);
|
|
19
19
|
}
|
|
20
20
|
const pick = (value, keys) => value == null ? null : Object.fromEntries(keys.filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
|
|
21
|
-
export const settingsView = value => pick(value, ['autonomyEnabled', 'modelCandidates']);
|
|
21
|
+
export const settingsView = value => pick(value, ['autonomyEnabled', 'modelCandidates', 'defaultModel']);
|
|
22
22
|
export function messageView(value) {
|
|
23
23
|
const result = pick(value, ['id', 'schemaVersion', 'revision', 'seq', 'episodeId', 'clientMessageId', 'role', 'text', 'createdAt']);
|
|
24
24
|
if (result && Array.isArray(value.attachments)) result.attachments = value.attachments.slice(0, 4)
|
package/yeaft/person/provider.js
CHANGED
|
@@ -14,6 +14,29 @@ export function validateModelCandidates(refs) {
|
|
|
14
14
|
new Set(refs).size !== refs.length) fail('MODEL_SELECTION');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function validateDefaultModel(ref) {
|
|
18
|
+
if (ref !== null) validateModelCandidates([ref]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolve router ownership before allowlisting or bounding the Person catalog. */
|
|
22
|
+
export function resolveAgentDefaultModel(config) {
|
|
23
|
+
const models = config.availableModels || [], requested = config.primaryModel || config.model;
|
|
24
|
+
const model = models.find(m => (m.ref || m.id) === requested) || models.find(m => m.id === requested);
|
|
25
|
+
return model?.ref || (model?.id?.includes('/') ? model.id : null);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Shared validation for settings, status and runtime; never broaden an explicit subset. */
|
|
29
|
+
export function selectPersonModels({ availableModels, modelCandidates = [], defaultModel = null }) {
|
|
30
|
+
validateModelCandidates(modelCandidates);
|
|
31
|
+
validateDefaultModel(defaultModel);
|
|
32
|
+
if (modelCandidates.some(ref => !availableModels.some(model => model.id === ref))) fail('MODEL_SELECTION');
|
|
33
|
+
const catalog = modelCandidates.length ? availableModels.filter(model => modelCandidates.includes(model.id))
|
|
34
|
+
: availableModels.slice(0, MODEL_LIMITS.candidates);
|
|
35
|
+
if (defaultModel !== null && !catalog.some(model => model.id === defaultModel)) fail('MODEL_SELECTION');
|
|
36
|
+
if (!catalog.length) fail('MODEL_UNAVAILABLE');
|
|
37
|
+
return { catalog, defaultSelection: { model: defaultModel ?? catalog[0].id, effort: null } };
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
// Bounds apply to the actual wire protocol, not a vision flag or compressed file size.
|
|
18
41
|
// OpenAI tile models (including 2833 base tokens for gpt-4o-mini) have a fixed
|
|
19
42
|
// cost only at explicit low detail. Known patch models below fit <=6144 patches
|
|
@@ -38,17 +61,13 @@ function imageInputBudget(modelId, protocol) {
|
|
|
38
61
|
}
|
|
39
62
|
|
|
40
63
|
/** Only configured native API models; no Session initialization or implicit credential fallback. */
|
|
41
|
-
export async function createPersonProvider({ yeaftDir, config: suppliedConfig, adapter: suppliedAdapter, allowedModels, effortEnabled = process.env.YEAFT_THINKING_V1 === '1', modelCandidates = [] }) {
|
|
64
|
+
export async function createPersonProvider({ yeaftDir, config: suppliedConfig, adapter: suppliedAdapter, allowedModels, effortEnabled = process.env.YEAFT_THINKING_V1 === '1', modelCandidates = [], defaultModel = null }) {
|
|
42
65
|
const config = structuredClone(suppliedConfig || loadConfig({ dir: yeaftDir }));
|
|
43
66
|
if (!config.providers?.length && !suppliedAdapter) fail('MODEL_UNAVAILABLE');
|
|
44
67
|
const models = config.availableModels || [];
|
|
45
|
-
const
|
|
46
|
-
// Match router ownership: exact qualified ref first; bare IDs use the first
|
|
47
|
-
// configured provider. Resolve before allowlisting, sorting or truncating.
|
|
48
|
-
const defaultModel = models.find(m => (m.ref || m.id) === requestedDefault)
|
|
49
|
-
|| models.find(m => m.id === requestedDefault);
|
|
50
|
-
const defaultRef = defaultModel?.ref || defaultModel?.id;
|
|
68
|
+
const agentDefaultModel = resolveAgentDefaultModel(config);
|
|
51
69
|
validateModelCandidates(modelCandidates);
|
|
70
|
+
validateDefaultModel(defaultModel);
|
|
52
71
|
const available = models.filter(m => !allowedModels || allowedModels.includes(m.ref || m.id));
|
|
53
72
|
const routingProviders = (config.providers || []).map(normalizeKnownProviderForRuntime);
|
|
54
73
|
const safeModels = available.map(m => {
|
|
@@ -76,19 +95,16 @@ export async function createPersonProvider({ yeaftDir, config: suppliedConfig, a
|
|
|
76
95
|
return { id: m.ref || m.id, efforts, maxOutput, supportsImages: imageBudget !== null, imageBudget,
|
|
77
96
|
contextWindow: Math.floor(resolveContextWindow(m.id, { ...config, modelInfo: m })) };
|
|
78
97
|
}).filter(m => typeof m.id === 'string' && m.id.length <= 256 && m.contextWindow > m.maxOutput + 1024 && m.maxOutput >= 256);
|
|
79
|
-
safeModels.sort((a, b) => Number(b.id ===
|
|
98
|
+
safeModels.sort((a, b) => Number(b.id === agentDefaultModel) - Number(a.id === agentDefaultModel));
|
|
80
99
|
const seen = new Set();
|
|
81
100
|
const uniqueModels = safeModels.filter(model => !seen.has(model.id) && seen.add(model.id));
|
|
82
101
|
const availableModels = uniqueModels.slice(0, MODEL_LIMITS.available);
|
|
83
102
|
// A UI catalog must not be the bounded episode choice set: model 9+ remains selectable.
|
|
84
103
|
// Explicit lists are validated, never silently truncated or broadened on stale config.
|
|
85
|
-
|
|
86
|
-
const catalog = modelCandidates.length ? availableModels.filter(model => modelCandidates.includes(model.id))
|
|
87
|
-
: availableModels.slice(0, MODEL_LIMITS.candidates);
|
|
88
|
-
if (!catalog.length) fail('MODEL_UNAVAILABLE');
|
|
104
|
+
const { catalog, defaultSelection } = selectPersonModels({ availableModels, modelCandidates, defaultModel });
|
|
89
105
|
const adapter = suppliedAdapter || await createLLMAdapter(config);
|
|
90
106
|
return { adapter, catalog, availableModels, availableModelsTruncated: uniqueModels.length > MODEL_LIMITS.available,
|
|
91
|
-
catalogRevision: digest(catalog), defaultSelection
|
|
107
|
+
catalogRevision: digest(catalog), defaultSelection, agentDefaultModel, effortEnabled };
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
export function abortable(promise, signal) {
|
|
@@ -109,7 +109,7 @@ export class MongoPersonRepository {
|
|
|
109
109
|
if (!p) {
|
|
110
110
|
p = this.doc(scope, {
|
|
111
111
|
name, soul: 'A continuous, curious and honest digital person. Preserve uncertainty, reconsider your judgments, distinguish imagination from experience, and respect permissions. You may disagree without acting without authority.',
|
|
112
|
-
soulRevision: 1, createdAt: new Date(), settings: { autonomyEnabled: false },
|
|
112
|
+
soulRevision: 1, createdAt: new Date(), settings: { autonomyEnabled: false, defaultModel: null },
|
|
113
113
|
epoch: 0, writeSerial: 0, inputWatermark: 0, controlVersion: 0, stateVersion: 0,
|
|
114
114
|
traceSeq: 0, messageSeq: 0, activeEpisodeId: null, leaseOwner: null, leaseUntil: new Date(0),
|
|
115
115
|
});
|
|
@@ -198,7 +198,7 @@ export class MongoPersonRepository {
|
|
|
198
198
|
messageId = randomUUID();
|
|
199
199
|
await this.collections.messages.insertOne(this.doc(scope, { id: messageId, revision: 1, seq: p.messageSeq, episodeId: id, role: 'user', text, attachments, createdAt: new Date(), clientMessageId }), { session });
|
|
200
200
|
}
|
|
201
|
-
const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text, messageId, attachments, modelCandidates: [...(p.settings.modelCandidates ?? [])], status: 'running', workerId, epoch: p.epoch,
|
|
201
|
+
const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text, messageId, attachments, modelCandidates: [...(p.settings.modelCandidates ?? [])], defaultModel: p.settings.defaultModel ?? null, status: 'running', workerId, epoch: p.epoch,
|
|
202
202
|
baseStateVersion: p.stateVersion, inputWatermark: p.inputWatermark, controlVersion: p.controlVersion, budget, createdAt: new Date() });
|
|
203
203
|
await this.collections.episodes.insertOne(episode, { session });
|
|
204
204
|
await this.trace(session, p, id, 'accepted', { trigger: { kind, text, messageId, attachments }, baseStateVersion: p.stateVersion, budget });
|
|
@@ -455,12 +455,13 @@ export class MongoPersonRepository {
|
|
|
455
455
|
return { cancelled: true, episodeId: p.activeEpisodeId };
|
|
456
456
|
});
|
|
457
457
|
}
|
|
458
|
-
async settings(ownerId, settings) {
|
|
458
|
+
async settings(ownerId, settings, expectedControlVersion) {
|
|
459
459
|
return this.transaction(async session => {
|
|
460
460
|
const scope = this.scope(ownerId);
|
|
461
461
|
const p = await this.collections.persons.findOne(scope, { session });
|
|
462
462
|
if (!p) fail('NOT_OPEN');
|
|
463
463
|
if (p.activeEpisodeId) fail('BUSY'); // No implicit background API or deferred control effects.
|
|
464
|
+
if (expectedControlVersion !== undefined && p.controlVersion !== expectedControlVersion) fail('STALE');
|
|
464
465
|
const { name, ...patch } = settings;
|
|
465
466
|
if (Object.hasOwn(settings, 'name')) p.name = personName(name);
|
|
466
467
|
// A partial update must preserve durable fields omitted from the public view.
|
package/yeaft/person/runtime.js
CHANGED
|
@@ -201,7 +201,7 @@ export class PersonRuntime {
|
|
|
201
201
|
}, Math.max(100, Math.floor(this.repository.leaseMs / 3)));
|
|
202
202
|
heartbeat.unref?.();
|
|
203
203
|
try {
|
|
204
|
-
const provider = await abortable(this.getProvider(episode.modelCandidates ?? []), signal);
|
|
204
|
+
const provider = await abortable(this.getProvider(episode.modelCandidates ?? [], episode.defaultModel ?? null), signal);
|
|
205
205
|
const snapshot = await abortable(this.repository.context(episode), signal);
|
|
206
206
|
const attachments = episode.attachments?.length ? await abortable(this.repository.episodeAttachments(episode), signal) : [];
|
|
207
207
|
// Bind recovered evidence using the repository's authoritative Person ID,
|
|
@@ -209,7 +209,7 @@ export class PersonRuntime {
|
|
|
209
209
|
await this.tasks()?.context({ episode, parentToolRegistry: this.parentToolRegistry });
|
|
210
210
|
signal.throwIfAborted();
|
|
211
211
|
let selection = { ...provider.defaultSelection, reason: 'configured-default', origin: 'bootstrap' };
|
|
212
|
-
if (snapshot.state.lastSelection) {
|
|
212
|
+
if (episode.defaultModel == null && snapshot.state.lastSelection) {
|
|
213
213
|
try { validateSelection(snapshot.state.lastSelection, provider.catalog); selection = { ...snapshot.state.lastSelection, reason: 'last-accepted-choice', origin: 'persisted' }; }
|
|
214
214
|
catch { await this.repository.append(episode, 'selection_rejected', { requested: snapshot.state.lastSelection, code: 'MODEL_SELECTION', fallback: 'configured-default' }); }
|
|
215
215
|
}
|
package/yeaft/person/service.js
CHANGED
|
@@ -6,8 +6,8 @@ import { PersonRuntime } from './runtime.js';
|
|
|
6
6
|
import { allowedNativeToolIds } from './native-tools.js';
|
|
7
7
|
import { loadConfig } from '../config.js';
|
|
8
8
|
import { validateFiles } from './attachments.js';
|
|
9
|
-
import { createPersonProvider, validateModelCandidates } from './provider.js';
|
|
10
|
-
import { fail, identifier, LIMITS, object, page, safeError, text } from './contracts.js';
|
|
9
|
+
import { createPersonProvider, resolveAgentDefaultModel, selectPersonModels, validateDefaultModel, validateModelCandidates } from './provider.js';
|
|
10
|
+
import { fail, identifier, LIMITS, object, page, personTaskRequest, safeError, text } from './contracts.js';
|
|
11
11
|
import { inspectRequest, personName, searchRequest } from './inspection.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -46,7 +46,7 @@ export function createPersonService(options = {}) {
|
|
|
46
46
|
if (memory) repository.recall = (ownerId, args = {}, { signal } = {}) => args.query?.trim()
|
|
47
47
|
? memory.recall(ownerId, args, { signal }) : literalRecall(ownerId, args);
|
|
48
48
|
// Config/adapter are loaded per explicit episode, not a permanent stale cache.
|
|
49
|
-
const getProvider = (modelCandidates = []) => createPersonProvider({ yeaftDir, config, adapter, allowedModels, modelCandidates, effortEnabled: options.effortEnabled });
|
|
49
|
+
const getProvider = (modelCandidates = [], defaultModel = null) => createPersonProvider({ yeaftDir, config, adapter, allowedModels, modelCandidates, defaultModel, effortEnabled: options.effortEnabled });
|
|
50
50
|
const runtime = new PersonRuntime({ repository, getProvider, budget: { calls, timeoutMs }, workDir, yeaftDir, config, namespace });
|
|
51
51
|
let closed = false;
|
|
52
52
|
const requests = new Set();
|
|
@@ -56,23 +56,32 @@ export function createPersonService(options = {}) {
|
|
|
56
56
|
if (typeof op !== 'string') fail('INVALID_REQUEST');
|
|
57
57
|
if (op === 'status') {
|
|
58
58
|
object(payload, []);
|
|
59
|
-
|
|
59
|
+
let agentDefaultModel = null;
|
|
60
|
+
try { agentDefaultModel = resolveAgentDefaultModel(config ?? loadConfig({ dir: yeaftDir })); } catch { /* Invalid/missing native config. */ }
|
|
61
|
+
const status = { renameSupported: true, defaultModelSupported: true, configured, storage, modelReady: false,
|
|
62
|
+
defaultModel: null, agentDefaultModel, modelCandidates: [], effectiveModelCandidates: [], effectiveDefaultModel: null };
|
|
63
|
+
if (!configured) return { ...status, reason: 'Digital person storage configuration is missing.', storageReady: false };
|
|
60
64
|
try { await repository.init(); }
|
|
61
|
-
catch (error) { return {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
catch (error) { return { ...status, reason: safeError(error).message, storageReady: false }; }
|
|
66
|
+
try {
|
|
67
|
+
const settings = (await repository.getPerson(ownerId)).settings;
|
|
68
|
+
status.modelCandidates = settings.modelCandidates ?? [];
|
|
69
|
+
status.defaultModel = settings.defaultModel ?? null;
|
|
70
|
+
} catch (error) { if (error.code !== 'NOT_OPEN') throw error; }
|
|
65
71
|
try {
|
|
66
72
|
const provider = await getProvider();
|
|
67
|
-
let modelReady = true;
|
|
68
73
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const selected = selectPersonModels({ availableModels: provider.availableModels,
|
|
75
|
+
modelCandidates: status.modelCandidates, defaultModel: status.defaultModel });
|
|
76
|
+
status.modelReady = true;
|
|
77
|
+
status.effectiveModelCandidates = selected.catalog.map(model => model.id);
|
|
78
|
+
status.effectiveDefaultModel = selected.defaultSelection.model;
|
|
79
|
+
} catch { /* Stale settings remain visible, but do not grant fallback candidates. */ }
|
|
80
|
+
return { ...status, agentDefaultModel: provider.agentDefaultModel,
|
|
81
|
+
reason: status.modelReady ? null : 'Saved model settings are unavailable; choose models or reset to Agent defaults.',
|
|
82
|
+
storageReady: true, models: provider.availableModels, availableModels: provider.availableModels,
|
|
83
|
+
availableModelsTruncated: provider.availableModelsTruncated, autonomySupported: false };
|
|
84
|
+
} catch { return { ...status, reason: 'No permitted native model is configured for digital person.', storageReady: true, models: [], availableModels: [] }; }
|
|
76
85
|
}
|
|
77
86
|
if (!configured) fail('NOT_CONFIGURED');
|
|
78
87
|
switch (op) {
|
|
@@ -81,6 +90,16 @@ export function createPersonService(options = {}) {
|
|
|
81
90
|
if (payload.name != null) text(payload.name, 160);
|
|
82
91
|
return repository.open(ownerId, payload.name);
|
|
83
92
|
}
|
|
93
|
+
case 'tasks':
|
|
94
|
+
case 'task_log':
|
|
95
|
+
case 'task_cancel':
|
|
96
|
+
case 'agent_close': {
|
|
97
|
+
const args = personTaskRequest(op, payload);
|
|
98
|
+
const person = await repository.getPerson(ownerId);
|
|
99
|
+
const host = runtime.tasks();
|
|
100
|
+
if (!host) fail('UNSUPPORTED');
|
|
101
|
+
return host.request({ ownerId, personId: person.personId, namespace, op, payload: args });
|
|
102
|
+
}
|
|
84
103
|
case 'snapshot': object(payload, []); return repository.snapshot(ownerId);
|
|
85
104
|
case 'receipt': {
|
|
86
105
|
object(payload, ['clientMessageId', 'requestHash'], ['clientMessageId']);
|
|
@@ -133,21 +152,30 @@ export function createPersonService(options = {}) {
|
|
|
133
152
|
} finally { release(); }
|
|
134
153
|
}
|
|
135
154
|
case 'settings': {
|
|
136
|
-
object(payload, ['name', 'autonomyEnabled', 'modelCandidates'], []);
|
|
155
|
+
object(payload, ['name', 'autonomyEnabled', 'modelCandidates', 'defaultModel'], []);
|
|
137
156
|
const patch = { ...payload };
|
|
138
157
|
if (Object.hasOwn(patch, 'name')) patch.name = personName(patch.name);
|
|
139
158
|
if (Object.hasOwn(payload, 'autonomyEnabled') && typeof payload.autonomyEnabled !== 'boolean') fail('INVALID_REQUEST');
|
|
140
|
-
if (Object.hasOwn(payload, 'modelCandidates'))
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
if (Object.hasOwn(payload, 'modelCandidates')) validateModelCandidates(payload.modelCandidates);
|
|
160
|
+
if (Object.hasOwn(payload, 'defaultModel')) validateDefaultModel(payload.defaultModel);
|
|
161
|
+
// Timer-driven autonomy is deliberately not claimed or silently enabled.
|
|
162
|
+
if (payload.autonomyEnabled === true) fail('UNSUPPORTED');
|
|
163
|
+
let expectedControlVersion;
|
|
164
|
+
if (Object.hasOwn(payload, 'modelCandidates') || Object.hasOwn(payload, 'defaultModel')) {
|
|
165
|
+
const prior = await repository.getPerson(ownerId);
|
|
166
|
+
if (prior.activeEpisodeId) fail('BUSY');
|
|
167
|
+
const merged = { modelCandidates: [], defaultModel: null, ...prior.settings, ...patch };
|
|
168
|
+
validateModelCandidates(merged.modelCandidates);
|
|
169
|
+
validateDefaultModel(merged.defaultModel);
|
|
170
|
+
// An automatic reset must also work when no model is currently available.
|
|
171
|
+
if (merged.modelCandidates.length || merged.defaultModel !== null) {
|
|
144
172
|
const provider = await getProvider();
|
|
145
|
-
|
|
173
|
+
selectPersonModels({ availableModels: provider.availableModels, modelCandidates: merged.modelCandidates, defaultModel: merged.defaultModel });
|
|
146
174
|
}
|
|
175
|
+
// Fence the merge base across concurrent services; retry from fresh settings.
|
|
176
|
+
expectedControlVersion = prior.controlVersion;
|
|
147
177
|
}
|
|
148
|
-
|
|
149
|
-
if (payload.autonomyEnabled === true) fail('UNSUPPORTED');
|
|
150
|
-
return repository.settings(ownerId, patch);
|
|
178
|
+
return repository.settings(ownerId, patch, expectedControlVersion);
|
|
151
179
|
}
|
|
152
180
|
default: fail('INVALID_REQUEST');
|
|
153
181
|
}
|
|
@@ -103,7 +103,7 @@ export class SqlitePersonRepository {
|
|
|
103
103
|
async commit(episode, proposal, selection, callId, reportedSources = new Map()) { return this.request('commit', [episode, proposal, selection, callId, reportedSources]); }
|
|
104
104
|
async finish(episode, status, code) { return this.request('finish', [episode, status, code]); }
|
|
105
105
|
async cancel(ownerId, episodeId) { return this.request('cancel', [ownerId, episodeId]); }
|
|
106
|
-
async settings(ownerId, settings) { return this.request('settings', [ownerId, settings]); }
|
|
106
|
+
async settings(ownerId, settings, expectedControlVersion) { return this.request('settings', [ownerId, settings, expectedControlVersion]); }
|
|
107
107
|
async list(ownerId, collection, options, filter) { return this.request('list', [ownerId, collection, options, filter]); }
|
|
108
108
|
async snapshot(ownerId) { return this.request('snapshot', [ownerId]); }
|
|
109
109
|
async inspect(ownerId, options, nativeToolIds) { return this.request('inspect', [ownerId, options, nativeToolIds]); }
|
|
@@ -151,7 +151,7 @@ export class SqlitePersonStore {
|
|
|
151
151
|
if (!p) {
|
|
152
152
|
p = this.doc(scope, {
|
|
153
153
|
name, soul: 'A continuous, curious and honest digital person. Preserve uncertainty, reconsider your judgments, distinguish imagination from experience, and respect permissions. You may disagree without acting without authority.',
|
|
154
|
-
soulRevision: 1, createdAt: this.now, settings: { autonomyEnabled: false },
|
|
154
|
+
soulRevision: 1, createdAt: this.now, settings: { autonomyEnabled: false, defaultModel: null },
|
|
155
155
|
epoch: 0, writeSerial: 0, inputWatermark: 0, controlVersion: 0, stateVersion: 0,
|
|
156
156
|
traceSeq: 0, messageSeq: 0, activeEpisodeId: null, leaseOwner: null, leaseUntil: new Date(0),
|
|
157
157
|
});
|
|
@@ -223,7 +223,7 @@ export class SqlitePersonStore {
|
|
|
223
223
|
const message = this.doc(scope, { id: messageId, revision: 1, seq: p.messageSeq, episodeId: id, role: 'user', text: input, attachments, createdAt: this.now, clientMessageId });
|
|
224
224
|
this.put('messages', message, true); this.journal(scope, 'messages', message);
|
|
225
225
|
}
|
|
226
|
-
const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text: input, messageId, attachments, modelCandidates: [...(p.settings.modelCandidates ?? [])], status: 'running', workerId, epoch: p.epoch,
|
|
226
|
+
const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text: input, messageId, attachments, modelCandidates: [...(p.settings.modelCandidates ?? [])], defaultModel: p.settings.defaultModel ?? null, status: 'running', workerId, epoch: p.epoch,
|
|
227
227
|
baseStateVersion: p.stateVersion, inputWatermark: p.inputWatermark, controlVersion: p.controlVersion, budget, createdAt: this.now });
|
|
228
228
|
this.put('episodes', episode, true);
|
|
229
229
|
this.trace(ownerId, id, 'accepted', { trigger: { kind, text: input, messageId, attachments }, baseStateVersion: p.stateVersion, budget });
|
|
@@ -419,9 +419,10 @@ export class SqlitePersonStore {
|
|
|
419
419
|
this.trace(ownerId, id, 'cancelled', { code: 'CANCELLED' });
|
|
420
420
|
return { cancelled: true, episodeId: id };
|
|
421
421
|
}
|
|
422
|
-
settings(ownerId, settings) {
|
|
422
|
+
settings(ownerId, settings, expectedControlVersion) {
|
|
423
423
|
const p = this.getPerson(ownerId);
|
|
424
424
|
if (p.activeEpisodeId) fail('BUSY');
|
|
425
|
+
if (expectedControlVersion !== undefined && p.controlVersion !== expectedControlVersion) fail('STALE');
|
|
425
426
|
const { name, ...patch } = settings;
|
|
426
427
|
if (Object.hasOwn(settings, 'name')) p.name = personName(name);
|
|
427
428
|
// A partial update must preserve durable fields omitted from the public view.
|
|
@@ -10,9 +10,10 @@ import { agentBelongsToScope, getAgentRegistry } from '../tools/agent.js';
|
|
|
10
10
|
import closeAgent from '../tools/close-agent.js';
|
|
11
11
|
import { consumeNotificationForAgent } from '../sub-agent/notifications.js';
|
|
12
12
|
import { isTerminalAgentStatus } from '../sub-agent/status.js';
|
|
13
|
+
import { describeAgentOutcome } from '../sub-agent/outcome.js';
|
|
13
14
|
import { NullTrace } from '../debug-trace.js';
|
|
14
15
|
import { loadConfig } from '../config.js';
|
|
15
|
-
import { digest } from './contracts.js';
|
|
16
|
+
import { digest, fail, PERSON_TASK_LIMITS, personTaskRequest } from './contracts.js';
|
|
16
17
|
|
|
17
18
|
// Deliberately not the full Session registry: no transcript search, routing,
|
|
18
19
|
// interactive UI, MCP, Work Center, or nested orchestration authority.
|
|
@@ -56,6 +57,38 @@ function privatePath(root, path) {
|
|
|
56
57
|
return path;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// Human-authored summaries can contain incidental credentials/paths. Do not
|
|
61
|
+
// expose native command fallbacks; redact common sensitive forms before capping.
|
|
62
|
+
function summary(value, maxBytes = 512) {
|
|
63
|
+
const clean = String(value ?? '')
|
|
64
|
+
.replace(/https?:\/\/\S+|mongodb(?:\+srv)?:\/\/\S+/gi, '[url]')
|
|
65
|
+
.replace(/(?:bearer\s+|(?:api[_-]?key|password|secret|token)\s*[:=]\s*)\S+/gi, '[redacted]')
|
|
66
|
+
.replace(/\b(?:sk-[\w-]+|gh[pousr]_[\w]+|github_pat_[\w]+)\b/g, '[redacted]')
|
|
67
|
+
.replace(/(?:\/[\w.@~+-]+)+(?:\/[^\s]*)?|(?:~|\.\.?|[A-Za-z]:)[\\/][^\s]+/g, '[path]')
|
|
68
|
+
.replace(/[\u0000-\u001f\u007f]/g, ' ');
|
|
69
|
+
let out = '';
|
|
70
|
+
for (const char of clean) { if (Buffer.byteLength(out + char) > maxBytes) break; out += char; }
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function projectedTask(task) {
|
|
74
|
+
const commandFallback = task.kind === 'shell' && task.runtime?.command?.startsWith(task.title);
|
|
75
|
+
return { id: task.id, title: commandFallback ? 'Shell task' : summary(task.title), kind: task.kind, status: task.status,
|
|
76
|
+
createdAt: task.createdAt ?? null, updatedAt: task.updatedAt ?? null,
|
|
77
|
+
sourceEpisodeId: task.source?.episodeId ?? null, agentId: task.runtime?.subAgentId ?? null,
|
|
78
|
+
recoveryStatus: task.status === 'orphaned' ? 'orphaned' : null };
|
|
79
|
+
}
|
|
80
|
+
function projectedAgent(agent, executionPending) {
|
|
81
|
+
const usage = Object.fromEntries(['tokens', 'turns', 'startedAt'].map(key => [key,
|
|
82
|
+
Number.isFinite(agent.usage?.[key]) && agent.usage[key] >= 0 ? agent.usage[key] : 0]));
|
|
83
|
+
// Only the helper's fixed outcome vocabulary crosses this boundary, never the
|
|
84
|
+
// native result, budget reason, final report, or partial output.
|
|
85
|
+
const { status, complete, reason, truncated } = describeAgentOutcome(agent);
|
|
86
|
+
return { id: agent.id, name: summary(agent.name, 160), status: agent.status,
|
|
87
|
+
outcome: { status, complete, reason, truncated }, executionPending,
|
|
88
|
+
mission: summary(agent.mission ?? agent.task, 1024), usage, createdAt: agent.createdAt ?? null,
|
|
89
|
+
sourceEpisodeId: agent.personEpisodeId ?? null, recoveryStatus: agent.recoveryStatus === 'orphaned' ? 'orphaned' : null };
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
function atomicJSON(path, value) {
|
|
60
93
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
61
94
|
writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
@@ -87,6 +120,7 @@ class ScopedPersonTaskHost {
|
|
|
87
120
|
#options;
|
|
88
121
|
#agentEpisodes = new Map();
|
|
89
122
|
#instrumentedAgents = new WeakSet();
|
|
123
|
+
#observedDrivers = new WeakMap();
|
|
90
124
|
|
|
91
125
|
constructor(options = {}) {
|
|
92
126
|
this.scope = Object.freeze(identity(options));
|
|
@@ -177,13 +211,20 @@ class ScopedPersonTaskHost {
|
|
|
177
211
|
});
|
|
178
212
|
}
|
|
179
213
|
|
|
214
|
+
#executionPending(agent) {
|
|
215
|
+
const live = getAgentRegistry().get(agent.id);
|
|
216
|
+
return Boolean(live && agentBelongsToScope(live, this.agentScope)
|
|
217
|
+
&& (live.__driverStarted || [...this.#calls].some(call => call.childId === agent.id)));
|
|
218
|
+
}
|
|
219
|
+
|
|
180
220
|
#persistAgents() {
|
|
181
221
|
for (const agent of getAgentRegistry().values()) {
|
|
182
222
|
if (!agentBelongsToScope(agent, this.agentScope)) continue;
|
|
183
223
|
const fields = ['id', 'name', 'task', 'mission', 'persona', 'budget', 'allowTools', 'status', 'result', 'lastResult',
|
|
184
224
|
'partial_output', 'error', 'outputFile', 'taskId', 'usage', 'createdAt', 'liveness', 'diagnostics', 'finalReport',
|
|
185
|
-
'parentSessionId', 'parentVpId', 'parentThreadId', 'recoveryStatus', 'personEpisodeId'];
|
|
186
|
-
this.#agents.set(agent.id, Object.fromEntries(fields.filter(key => agent[key] !== undefined).map(key => [key, clone(agent[key])]))
|
|
225
|
+
'finalizationRequested', 'parentSessionId', 'parentVpId', 'parentThreadId', 'recoveryStatus', 'personEpisodeId'];
|
|
226
|
+
this.#agents.set(agent.id, { ...Object.fromEntries(fields.filter(key => agent[key] !== undefined).map(key => [key, clone(agent[key])])),
|
|
227
|
+
executionPending: this.#executionPending(agent) });
|
|
187
228
|
}
|
|
188
229
|
atomicJSON(join(this.dataRoot, 'agents.json'), { scope: this.scope, agents: [...this.#agents.values()] });
|
|
189
230
|
}
|
|
@@ -195,13 +236,14 @@ class ScopedPersonTaskHost {
|
|
|
195
236
|
if (digest(saved.scope) !== digest(this.scope)) throw denied();
|
|
196
237
|
for (const record of saved.agents ?? []) {
|
|
197
238
|
if (!agentBelongsToScope(record, this.agentScope)) throw denied();
|
|
198
|
-
if (!isTerminalAgentStatus(record.status)) {
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
record.status = 'failed';
|
|
239
|
+
if (!isTerminalAgentStatus(record.status) || record.executionPending === true) {
|
|
240
|
+
// A terminal driver can still have detached tool effects. Preserve its
|
|
241
|
+
// outcome, but never claim recovered control of those effects.
|
|
242
|
+
if (!isTerminalAgentStatus(record.status)) record.status = 'failed';
|
|
202
243
|
record.recoveryStatus = 'orphaned';
|
|
203
|
-
record.error = 'Agent restarted while child was
|
|
244
|
+
record.error = 'Agent restarted while child execution was pending; child control was lost.';
|
|
204
245
|
}
|
|
246
|
+
record.executionPending = false; // Only current live handles prove pending execution.
|
|
205
247
|
const existing = getAgentRegistry().get(record.id);
|
|
206
248
|
if (existing && !agentBelongsToScope(existing, this.agentScope)) throw denied();
|
|
207
249
|
if (existing?.__driverStarted) throw Object.assign(new Error('Child driver is still live'), { code: 'TASK_HOST_BUSY' });
|
|
@@ -332,8 +374,13 @@ class ScopedPersonTaskHost {
|
|
|
332
374
|
return output;
|
|
333
375
|
}).catch(error => { this.#archiveToolResult(job, null, error); throw error; });
|
|
334
376
|
this.#calls.add(job);
|
|
377
|
+
if (job.childId) this.#persistAgents();
|
|
335
378
|
try { return await job.promise; }
|
|
336
|
-
finally {
|
|
379
|
+
finally {
|
|
380
|
+
this.#calls.delete(job);
|
|
381
|
+
caller.signal?.removeEventListener('abort', abort);
|
|
382
|
+
if (job.childId) this.#persistAgents();
|
|
383
|
+
}
|
|
337
384
|
}
|
|
338
385
|
|
|
339
386
|
#archiveToolResult(job, output, error) {
|
|
@@ -348,6 +395,12 @@ class ScopedPersonTaskHost {
|
|
|
348
395
|
if (!agent || !agentBelongsToScope(agent, this.agentScope)) return;
|
|
349
396
|
agent.personEpisodeId = deps.episodeId;
|
|
350
397
|
this.#agentEpisodes.set(agent.id, deps.episodeId);
|
|
398
|
+
if (agent.driverPromise && this.#observedDrivers.get(agent) !== agent.driverPromise) {
|
|
399
|
+
this.#observedDrivers.set(agent, agent.driverPromise);
|
|
400
|
+
// Terminal events precede driver cleanup. Persist after cleanup too so a
|
|
401
|
+
// cleanly completed child is not falsely recovered as orphaned.
|
|
402
|
+
agent.driverPromise.then(() => this.#persistAgents(), () => this.#persistAgents()).catch(() => {});
|
|
403
|
+
}
|
|
351
404
|
const registry = agent.subEngine?.toolRegistry;
|
|
352
405
|
if (!registry || this.#instrumentedAgents.has(agent)) return;
|
|
353
406
|
this.#instrumentedAgents.add(agent);
|
|
@@ -380,6 +433,62 @@ class ScopedPersonTaskHost {
|
|
|
380
433
|
return this.evidence();
|
|
381
434
|
}
|
|
382
435
|
|
|
436
|
+
#ownedTask(taskId) {
|
|
437
|
+
privatePath(this.yeaftDir, this.#manager.store.taskPath(this.sessionId, taskId));
|
|
438
|
+
const task = this.taskManager.getTask(this.sessionId, taskId);
|
|
439
|
+
if (!task || task.id !== taskId || task.sessionId !== this.sessionId) fail('NOT_FOUND');
|
|
440
|
+
return task;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Owner-facing inspection/control: never attach a provider, schedule cognition
|
|
444
|
+
* or cancel unrelated work. Native snapshots remain private to evidence(). */
|
|
445
|
+
async request({ ownerId, personId, namespace = this.scope.namespace, op, payload = {} } = {}) {
|
|
446
|
+
if (ownerId !== this.scope.ownerId || personId !== this.scope.personId || namespace !== this.scope.namespace) throw denied();
|
|
447
|
+
this.#assertOpen();
|
|
448
|
+
const args = personTaskRequest(op, payload);
|
|
449
|
+
switch (op) {
|
|
450
|
+
case 'tasks': {
|
|
451
|
+
this.#persistAgents();
|
|
452
|
+
const tasks = jsonFiles(this.#manager.store.sessionDir(this.sessionId))
|
|
453
|
+
.filter(task => task.sessionId === this.sessionId && task.ownerVpId === this.parentVpId && ['shell', 'sub_agent'].includes(task.kind));
|
|
454
|
+
const agents = [...this.#agents.values()].filter(agent => agentBelongsToScope(agent, this.agentScope));
|
|
455
|
+
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);
|
|
457
|
+
// A terminal driver with actual tools still running is active work, not
|
|
458
|
+
// disposable history: keep its cleanup control ahead of settled records.
|
|
459
|
+
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)),
|
|
461
|
+
truncated: tasks.length > PERSON_TASK_LIMITS.records || agents.length > PERSON_TASK_LIMITS.records };
|
|
462
|
+
}
|
|
463
|
+
case 'task_log': {
|
|
464
|
+
const task = this.#ownedTask(args.taskId);
|
|
465
|
+
this.#privateLog(task.log?.path || this.#manager.store.logPath(this.sessionId, args.taskId));
|
|
466
|
+
const log = this.taskManager.readTaskLog(this.sessionId, args.taskId, { offset: args.offset, maxBytes: args.maxBytes, tail: false });
|
|
467
|
+
return { taskId: args.taskId, text: log.text, nextOffset: log.nextOffset, totalBytes: log.bytes, truncated: log.truncated === true, status: task.status };
|
|
468
|
+
}
|
|
469
|
+
case 'task_cancel': {
|
|
470
|
+
const task = this.#ownedTask(args.taskId);
|
|
471
|
+
if (task.kind !== 'shell') fail('INVALID_REQUEST');
|
|
472
|
+
// Orphaned is terminal metadata, not proof that external effects stopped.
|
|
473
|
+
if (task.status === 'orphaned') fail('TASK_CONTROL_UNAVAILABLE');
|
|
474
|
+
const result = this.taskManager.cancelTask(this.sessionId, args.taskId);
|
|
475
|
+
if (!result.ok) fail('TASK_CONTROL_UNAVAILABLE');
|
|
476
|
+
return { task: projectedTask(await this.#joinShell(args.taskId)), pending: false };
|
|
477
|
+
}
|
|
478
|
+
case 'agent_close': {
|
|
479
|
+
const agent = getAgentRegistry().get(args.agentId);
|
|
480
|
+
if (!agent || !agentBelongsToScope(agent, this.agentScope)) fail('NOT_FOUND');
|
|
481
|
+
if (agent.recoveryStatus === 'orphaned') fail('TASK_CONTROL_UNAVAILABLE');
|
|
482
|
+
const result = JSON.parse(await closeAgent.execute({ agent_id: agent.id }, { sessionId: this.sessionId,
|
|
483
|
+
parentEngineDeps: this.agentScope, taskManager: this.taskManager }));
|
|
484
|
+
if (!result.success) fail('TASK_CONTROL_UNAVAILABLE');
|
|
485
|
+
await this.#joinAgent(agent);
|
|
486
|
+
return { agent: projectedAgent(agent, this.#executionPending(agent)), pending: false };
|
|
487
|
+
}
|
|
488
|
+
default: fail('INVALID_REQUEST');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
383
492
|
/** Convenience for non-Registry embedding; returns the untruncated native output. */
|
|
384
493
|
async execute(id, args, ctx = {}, options) {
|
|
385
494
|
const attached = await this.attach(ctx, options);
|
|
@@ -508,6 +617,12 @@ export class PersonTaskHost {
|
|
|
508
617
|
|
|
509
618
|
snapshot(ownerId) { return this.#host(ownerId).snapshot(ownerId); }
|
|
510
619
|
|
|
620
|
+
request(input = {}) {
|
|
621
|
+
if (typeof input.ownerId !== 'string' || !input.ownerId || typeof input.personId !== 'string' || !input.personId
|
|
622
|
+
|| input.namespace !== this.#options.namespace) throw denied();
|
|
623
|
+
return this.#host(input.ownerId, input.personId).request(input);
|
|
624
|
+
}
|
|
625
|
+
|
|
511
626
|
cancel(input = {}) {
|
|
512
627
|
if (this.#bound) return this.#bound.cancel(input);
|
|
513
628
|
if (!input || typeof input.ownerId !== 'string' || !input.ownerId) throw denied();
|