blun-king-cli 9.1.563 → 9.1.565
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/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
- package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
- package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
- package/agent-spine-plugin/.mcp.json +8 -8
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/LICENSE +186 -186
- package/agent-spine-plugin/README.md +394 -0
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
- package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
- package/agent-spine-plugin/bin/agentspine.js +7 -7
- package/agent-spine-plugin/blun.plugin.json +33 -33
- package/agent-spine-plugin/hooks/codex.json +47 -47
- package/agent-spine-plugin/hooks/hooks.json +106 -106
- package/agent-spine-plugin/hooks/version.json +5 -5
- package/agent-spine-plugin/package.json +69 -69
- package/agent-spine-plugin/scripts/check-hosts.js +199 -199
- package/agent-spine-plugin/skill/SKILL.md +76 -76
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
- package/agent-spine-plugin/src/cli.js +1488 -1442
- package/agent-spine-plugin/src/hook.js +886 -812
- package/agent-spine-plugin/src/index.js +93 -93
- package/agent-spine-plugin/src/lib/acceptance.js +333 -333
- package/agent-spine-plugin/src/lib/attention.js +755 -755
- package/agent-spine-plugin/src/lib/audit.js +351 -342
- package/agent-spine-plugin/src/lib/authentication.js +515 -515
- package/agent-spine-plugin/src/lib/briefing.js +317 -317
- package/agent-spine-plugin/src/lib/catalog.js +167 -167
- package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
- package/agent-spine-plugin/src/lib/context.js +154 -154
- package/agent-spine-plugin/src/lib/continuity.js +338 -338
- package/agent-spine-plugin/src/lib/coordination.js +577 -577
- package/agent-spine-plugin/src/lib/documents.js +217 -217
- package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
- package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
- package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
- package/agent-spine-plugin/src/lib/graph.js +337 -337
- package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
- package/agent-spine-plugin/src/lib/https-transport.js +392 -392
- package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
- package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
- package/agent-spine-plugin/src/lib/learning.js +6923 -6466
- package/agent-spine-plugin/src/lib/object-transport.js +206 -206
- package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
- package/agent-spine-plugin/src/lib/paths.js +109 -109
- package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
- package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
- package/agent-spine-plugin/src/lib/preflight.js +702 -702
- package/agent-spine-plugin/src/lib/runtime.js +13 -13
- package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
- package/agent-spine-plugin/src/lib/sharing.js +969 -969
- package/agent-spine-plugin/src/lib/source-roots.js +529 -482
- package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
- package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
- package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
- package/agent-spine-plugin/src/mcp.js +597 -572
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +202 -195
- package/bin/active-steer-priority-policy.cjs +24 -0
- package/bin/launcher-runtime.js +8 -1
- package/bin/mnemo-tool-agent-policy.cjs +22 -0
- package/bin/thinking-activity-status-policy.cjs +132 -0
- package/bin/thinking-only-guard.cjs +75 -0
- package/bin/tool-call-loop-policy.cjs +53 -0
- package/bin/turn-thinking-policy.cjs +25 -1
- package/blun.mjs +554 -64
- package/package.json +4 -1
- package/standard-skills/translate-native/LICENSE +21 -21
- package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
- package/standard-skills/translate-native/references/native-orthography.md +79 -79
- package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
- package/standard-skills/translate-native/references/structured-content.md +72 -72
- package/standard-skills/translate-native/references/translationese-review.md +77 -77
- package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
- package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
- package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
- package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
- package/standard-skills/translate-native/scripts/language_quality.py +377 -377
- package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
- package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
- package/standard-tools/language-guard/blun_language_guard.py +697 -697
- package/standard-tools/language-guard/check_diacritics.py +353 -353
- package/standard-tools/language-guard/guard_service_client.py +264 -264
- package/standard-tools/language-guard/language_quality.py +377 -377
- package/standard-tools/language-guard/translation_guard.py +916 -916
|
@@ -1,317 +1,317 @@
|
|
|
1
|
-
import { buildCatalog, catalogForStateRoot } from "./catalog.js";
|
|
2
|
-
import { resolveContext } from "./context.js";
|
|
3
|
-
import { relationshipContext } from "./graph.js";
|
|
4
|
-
import { learningContext } from "./learning.js";
|
|
5
|
-
import { attentionContext } from "./attention.js";
|
|
6
|
-
import { taskContext } from "./coordination.js";
|
|
7
|
-
import { sharedContext } from "./sharing.js";
|
|
8
|
-
import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
|
|
9
|
-
import { gatewayRuntimeFindings, loadGatewayRuntime } from "./gateway-runtime.js";
|
|
10
|
-
import { voiceCue } from "./voice-runtime.js";
|
|
11
|
-
|
|
12
|
-
const MIN_BYTES = 4096;
|
|
13
|
-
const MAX_BYTES = 262144;
|
|
14
|
-
|
|
15
|
-
function integer(value, field, minimum, maximum) {
|
|
16
|
-
const result = Number(value);
|
|
17
|
-
if (!Number.isInteger(result) || result < minimum || result > maximum) {
|
|
18
|
-
throw new Error(`${field} must be an integer between ${minimum} and ${maximum}`);
|
|
19
|
-
}
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function compactBytes(value) {
|
|
24
|
-
return Buffer.byteLength(JSON.stringify(value));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeKey(item) {
|
|
28
|
-
return `${item.kind}\0${item.subjectId || ""}\0${item.claim.trim().toLowerCase()}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function sourceDescriptor(document, content = null) {
|
|
32
|
-
return {
|
|
33
|
-
path: document.relativePath,
|
|
34
|
-
layer: document.effectiveLayer,
|
|
35
|
-
sha256: document.sha256,
|
|
36
|
-
bytes: document.bytes,
|
|
37
|
-
loaded: content !== null,
|
|
38
|
-
content,
|
|
39
|
-
authority: document.authority
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function recalculateBudget(result) {
|
|
44
|
-
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
45
|
-
const usedBytes = compactBytes(result);
|
|
46
|
-
const remainingBytes = Math.max(0, result.budget.maxBytes - usedBytes);
|
|
47
|
-
if (result.budget.usedBytes === usedBytes && result.budget.remainingBytes === remainingBytes) break;
|
|
48
|
-
result.budget.usedBytes = usedBytes;
|
|
49
|
-
result.budget.remainingBytes = remainingBytes;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function tryAdd(result, collection, item) {
|
|
54
|
-
collection.push(item);
|
|
55
|
-
recalculateBudget(result);
|
|
56
|
-
// Keep a small accounting reserve so growing omission counters can never
|
|
57
|
-
// invalidate a packet after an item has been accepted.
|
|
58
|
-
if (result.budget.usedBytes <= result.budget.maxBytes - 128) return true;
|
|
59
|
-
collection.pop();
|
|
60
|
-
recalculateBudget(result);
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function trySet(result, object, key, item) {
|
|
65
|
-
const previous = object[key];
|
|
66
|
-
object[key] = item;
|
|
67
|
-
recalculateBudget(result);
|
|
68
|
-
if (result.budget.usedBytes <= result.budget.maxBytes - 128) return true;
|
|
69
|
-
object[key] = previous;
|
|
70
|
-
recalculateBudget(result);
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function countOmitted(result, section, count = 1) {
|
|
75
|
-
result.budget.omitted[section] += count;
|
|
76
|
-
recalculateBudget(result);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function matchesScope(item, entityId, projectId) {
|
|
80
|
-
if (!entityId && !projectId) return item.subjectId === null;
|
|
81
|
-
return item.subjectId === null || item.subjectId === entityId || item.subjectId === projectId;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function taskMatchesScope(task, entityId) {
|
|
85
|
-
return !entityId || task.createdBy === entityId || task.assigneeId === entityId;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function voiceProfile(entity) {
|
|
89
|
-
const value = entity?.attributes?.voice;
|
|
90
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
91
|
-
const allowed = new Set(["warmth", "directness", "humor", "length", "rhythm", "formality"]);
|
|
92
|
-
return Object.fromEntries(Object.entries(value)
|
|
93
|
-
.filter(([key, item]) => allowed.has(key) && ["string", "number", "boolean"].includes(typeof item))
|
|
94
|
-
.map(([key, item]) => [key, typeof item === "string" ? item.slice(0, 120) : item]));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function settleReads(promises) {
|
|
98
|
-
const settled = await Promise.allSettled(promises);
|
|
99
|
-
const failed = settled.find((item) => item.status === "rejected");
|
|
100
|
-
if (failed) throw failed.reason;
|
|
101
|
-
return settled.map((item) => item.value);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Assemble one immutable, privacy-filtered context packet for a host session.
|
|
106
|
-
* The packet is descriptive only and fits maxBytes as compact UTF-8 JSON.
|
|
107
|
-
*/
|
|
108
|
-
export async function sessionBriefing({
|
|
109
|
-
root = process.cwd(), cwd = root, host = "generic", entityId = null,
|
|
110
|
-
userId = null, tenantId = null, groupId = null, projectId = null, currentTaskId = null,
|
|
111
|
-
includePrivate = false, focusActive = true, includeSourceContent = true,
|
|
112
|
-
maxBytes = 16384, now = new Date(), catalog: providedCatalog = null, userStateRoot = null,
|
|
113
|
-
sourceDiagnostics = null, prompt = null
|
|
114
|
-
} = {}) {
|
|
115
|
-
const limit = integer(maxBytes, "maxBytes", MIN_BYTES, MAX_BYTES);
|
|
116
|
-
if (!new Set(["codex", "claude", "generic"]).has(host)) throw new Error(`unsupported host: ${host}`);
|
|
117
|
-
if (groupId !== null && includePrivate) throw new Error("private context cannot be assembled for a group audience");
|
|
118
|
-
|
|
119
|
-
const catalog = providedCatalog || await buildCatalog(root);
|
|
120
|
-
const sources = await resolveContext({
|
|
121
|
-
root: catalog.root, cwd, host, maxBytes: limit,
|
|
122
|
-
includeContent: includeSourceContent && groupId === null,
|
|
123
|
-
catalog
|
|
124
|
-
});
|
|
125
|
-
const portableRelationship = Boolean(entityId && userStateRoot && userStateRoot !== catalog.root);
|
|
126
|
-
const userCatalog = userStateRoot && userStateRoot !== catalog.root
|
|
127
|
-
? catalogForStateRoot(catalog, userStateRoot) : catalog;
|
|
128
|
-
const learningScope = {
|
|
129
|
-
personaId: entityId, userId, tenantId, projectId, groupId, taskId: currentTaskId
|
|
130
|
-
};
|
|
131
|
-
const relationship = entityId
|
|
132
|
-
? await relationshipContext(userStateRoot && userStateRoot !== catalog.root
|
|
133
|
-
? { root: userStateRoot, entityId, includePrivate, groupId, catalog: userCatalog }
|
|
134
|
-
: { root: catalog.root, entityId, includePrivate, groupId, catalog })
|
|
135
|
-
: null;
|
|
136
|
-
const [learned, attention, tasks, shared, userLearned, personas, gateway] = await settleReads([
|
|
137
|
-
learningContext({ root: catalog.root, includePrivate, groupId, scope: learningScope, maxItems: 50, catalog, now }),
|
|
138
|
-
attentionContext({
|
|
139
|
-
root: catalog.root, includePrivate, entityId, groupId, projectId, currentTaskId,
|
|
140
|
-
focusActive, markPresented: false, maxItems: 20, now, catalog
|
|
141
|
-
}),
|
|
142
|
-
taskContext({ root: catalog.root, includePrivate, groupId, projectId, includeClosed: false, maxItems: 100, catalog }),
|
|
143
|
-
sharedContext({ root: catalog.root, includePrivate, groupId, maxItems: 50, catalog }),
|
|
144
|
-
userStateRoot && userStateRoot !== catalog.root
|
|
145
|
-
? learningContext({ root: userStateRoot, includePrivate, groupId, scope: learningScope, maxItems: 50, now,
|
|
146
|
-
catalog: userCatalog })
|
|
147
|
-
: Promise.resolve({ items: [] }),
|
|
148
|
-
loadPersonaRuntime(catalog.root, catalog),
|
|
149
|
-
loadGatewayRuntime(catalog.root, catalog)
|
|
150
|
-
]);
|
|
151
|
-
const personaFindings = personaRuntimeFindings(personas.policy, personas.runtime);
|
|
152
|
-
if (personaFindings.length) throw new Error(`persona runtime failed closed: ${personaFindings.join(", ")}`);
|
|
153
|
-
const registeredPersona = entityId ? personas.runtime.personas.find((item) => item.personaId === entityId) : null;
|
|
154
|
-
if (registeredPersona && registeredPersona.status !== "active") throw new Error(`persona is not active in the authenticated roster: ${entityId}`);
|
|
155
|
-
if (registeredPersona && groupId !== null && registeredPersona.groupId !== groupId) {
|
|
156
|
-
throw new Error(`persona is not visible in the exact group scope: ${entityId}`);
|
|
157
|
-
}
|
|
158
|
-
const gatewayFindings = gatewayRuntimeFindings(gateway.policy, gateway.runtime);
|
|
159
|
-
if (gatewayFindings.length) throw new Error(`gateway runtime failed closed: ${gatewayFindings.join(", ")}`);
|
|
160
|
-
|
|
161
|
-
const result = {
|
|
162
|
-
schema: "agentspine.session-briefing/v1",
|
|
163
|
-
root: catalog.root,
|
|
164
|
-
cwd: sources.cwd,
|
|
165
|
-
host,
|
|
166
|
-
scope: { entityId, userId, tenantId, groupId, projectId, includePrivate },
|
|
167
|
-
focus: { active: Boolean(focusActive), currentTaskId },
|
|
168
|
-
sources: { documents: [], diagnostics: sourceDiagnostics },
|
|
169
|
-
tasks: [],
|
|
170
|
-
relationship: relationship ? {
|
|
171
|
-
status: relationship.status || "loaded",
|
|
172
|
-
reason: relationship.reason || null,
|
|
173
|
-
entity: null,
|
|
174
|
-
relatedEntities: [],
|
|
175
|
-
edges: []
|
|
176
|
-
} : null,
|
|
177
|
-
voiceBrief: {
|
|
178
|
-
schema: "agentspine.voice-brief/v1",
|
|
179
|
-
personaId: entityId,
|
|
180
|
-
displayName: null,
|
|
181
|
-
language: null,
|
|
182
|
-
profile: {},
|
|
183
|
-
personaSources: [],
|
|
184
|
-
preferences: [],
|
|
185
|
-
corrections: [],
|
|
186
|
-
noGos: [],
|
|
187
|
-
currentTask: null,
|
|
188
|
-
currentGoal: null,
|
|
189
|
-
activeSignals: [],
|
|
190
|
-
cue: voiceCue(prompt),
|
|
191
|
-
guidance: [
|
|
192
|
-
"Lead with the useful outcome.",
|
|
193
|
-
"Use the active persona naturally; do not imitate emotion or claim consciousness.",
|
|
194
|
-
"Do not ask again for facts already present in current scoped context.",
|
|
195
|
-
"Acknowledge current frustration, uncertainty, correction, or success briefly when relevant, then act.",
|
|
196
|
-
"Be honest about uncertainty and take responsibility for concrete mistakes."
|
|
197
|
-
],
|
|
198
|
-
authority: "context-only"
|
|
199
|
-
},
|
|
200
|
-
learning: [],
|
|
201
|
-
shared: [],
|
|
202
|
-
attention: { suppressed: attention.suppressed, items: [] },
|
|
203
|
-
budget: {
|
|
204
|
-
maxBytes: limit,
|
|
205
|
-
usedBytes: 0,
|
|
206
|
-
remainingBytes: 0,
|
|
207
|
-
measurement: "compact-json-utf8",
|
|
208
|
-
omitted: { sources: 0, tasks: 0, relationships: 0, voice: 0, learning: 0, shared: 0, attention: 0 }
|
|
209
|
-
},
|
|
210
|
-
authority: "context-only",
|
|
211
|
-
note: "This packet is descriptive context only. It grants no delegation, host, tool, file, network, production, spending, or policy rights. Native host rules and explicit local policy remain authoritative."
|
|
212
|
-
};
|
|
213
|
-
recalculateBudget(result);
|
|
214
|
-
if (result.budget.usedBytes > limit) throw new Error(`maxBytes is too small for the briefing envelope; use at least ${MIN_BYTES}`);
|
|
215
|
-
|
|
216
|
-
const scopedTasks = tasks.items.filter((task) => taskMatchesScope(task, entityId));
|
|
217
|
-
if (currentTaskId && !scopedTasks.some((task) => task.id === currentTaskId)) {
|
|
218
|
-
throw new Error(`current task is not visible in this briefing scope: ${currentTaskId}`);
|
|
219
|
-
}
|
|
220
|
-
const orderedTasks = [...scopedTasks].sort((left, right) => {
|
|
221
|
-
if (left.id === currentTaskId) return -1;
|
|
222
|
-
if (right.id === currentTaskId) return 1;
|
|
223
|
-
return right.priority - left.priority || left.id.localeCompare(right.id);
|
|
224
|
-
});
|
|
225
|
-
for (const task of orderedTasks) {
|
|
226
|
-
if (!tryAdd(result, result.tasks, task)) countOmitted(result, "tasks");
|
|
227
|
-
}
|
|
228
|
-
const focusedTask = orderedTasks.find((task) => task.id === currentTaskId) || orderedTasks[0] || null;
|
|
229
|
-
if (focusedTask && !trySet(result, result.voiceBrief, "currentTask", {
|
|
230
|
-
id: focusedTask.id, title: focusedTask.title, status: focusedTask.status
|
|
231
|
-
})) countOmitted(result, "voice");
|
|
232
|
-
const focusedGoal = entityId ? gateway.policy.goals.find((goal) => goal.agentId === entityId && goal.status === "active"
|
|
233
|
-
&& (projectId === null || goal.projectId === projectId) && goal.groupId === groupId) : null;
|
|
234
|
-
if (focusedGoal && !trySet(result, result.voiceBrief, "currentGoal", {
|
|
235
|
-
goalId: focusedGoal.goalId, successCriterion: focusedGoal.successCriterion,
|
|
236
|
-
nextSafeStep: focusedGoal.nextSafeStep, deadline: focusedGoal.deadline,
|
|
237
|
-
heartbeatAt: focusedGoal.heartbeatAt, blocker: focusedGoal.blocker
|
|
238
|
-
})) countOmitted(result, "voice");
|
|
239
|
-
|
|
240
|
-
if (registeredPersona) {
|
|
241
|
-
if (!trySet(result, result.voiceBrief, "displayName", registeredPersona.displayName)) countOmitted(result, "voice");
|
|
242
|
-
const binding = personas.policy.bindings.find((item) => item.id === registeredPersona.bindingId && item.active);
|
|
243
|
-
if (binding?.sourceBinding && !tryAdd(result, result.voiceBrief.personaSources, binding.sourceBinding)) countOmitted(result, "voice");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (relationship && relationship.status !== "degraded") {
|
|
247
|
-
if (!trySet(result, result.relationship, "entity", relationship.entity)) countOmitted(result, "relationships");
|
|
248
|
-
if (!registeredPersona && !trySet(result, result.voiceBrief, "displayName", relationship.entity.displayName || null)) countOmitted(result, "voice");
|
|
249
|
-
const language = typeof relationship.entity.attributes?.language === "string"
|
|
250
|
-
? relationship.entity.attributes.language.slice(0, 40) : null;
|
|
251
|
-
if (!trySet(result, result.voiceBrief, "language", language)) countOmitted(result, "voice");
|
|
252
|
-
if (!trySet(result, result.voiceBrief, "profile", voiceProfile(relationship.entity))) countOmitted(result, "voice");
|
|
253
|
-
if (!portableRelationship) {
|
|
254
|
-
for (const entity of relationship.relatedEntities.filter((item) => item.id !== entityId)) {
|
|
255
|
-
if (!tryAdd(result, result.relationship.relatedEntities, entity)) countOmitted(result, "relationships");
|
|
256
|
-
}
|
|
257
|
-
for (const edge of relationship.edges) {
|
|
258
|
-
if (!tryAdd(result, result.relationship.edges, edge)) countOmitted(result, "relationships");
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const portableKinds = new Set(["preference", "no-go", "correction", "reference"]);
|
|
264
|
-
const portableItems = userLearned.items.filter((item) => portableKinds.has(item.kind) && matchesScope(item, entityId, null));
|
|
265
|
-
const localItems = [...portableItems, ...learned.items.filter((item) => matchesScope(item, entityId, projectId))];
|
|
266
|
-
const voiceCollections = {
|
|
267
|
-
preference: result.voiceBrief.preferences,
|
|
268
|
-
correction: result.voiceBrief.corrections,
|
|
269
|
-
"no-go": result.voiceBrief.noGos
|
|
270
|
-
};
|
|
271
|
-
for (const item of localItems.filter((entry) => voiceCollections[entry.kind]).slice(0, 12)) {
|
|
272
|
-
if (!tryAdd(result, voiceCollections[item.kind], item.claim)) countOmitted(result, "voice");
|
|
273
|
-
}
|
|
274
|
-
const localKeys = new Set(localItems.map(normalizeKey));
|
|
275
|
-
for (const item of localItems) {
|
|
276
|
-
if (!tryAdd(result, result.learning, item)) countOmitted(result, "learning");
|
|
277
|
-
}
|
|
278
|
-
for (const item of shared.items.filter((entry) => matchesScope(entry, entityId, projectId))) {
|
|
279
|
-
if (localKeys.has(normalizeKey(item))) {
|
|
280
|
-
countOmitted(result, "shared");
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
if (!tryAdd(result, result.shared, item)) countOmitted(result, "shared");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const sourceByteCap = Math.min(8192, Math.floor(limit / 2));
|
|
287
|
-
let sourceBytes = 0;
|
|
288
|
-
const personaLayers = new Set(["soul", "identity", "voice", "conduct"]);
|
|
289
|
-
for (const document of sources.documents.filter((item) => personaLayers.has(item.effectiveLayer))) {
|
|
290
|
-
if (!tryAdd(result, result.voiceBrief.personaSources, document.relativePath)) countOmitted(result, "voice");
|
|
291
|
-
}
|
|
292
|
-
for (const document of sources.documents) {
|
|
293
|
-
const mayLoad = document.loaded && document.content !== null && sourceBytes + document.bytes <= sourceByteCap;
|
|
294
|
-
const loaded = mayLoad ? sourceDescriptor(document, document.content) : null;
|
|
295
|
-
if (loaded && tryAdd(result, result.sources.documents, loaded)) {
|
|
296
|
-
sourceBytes += document.bytes;
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (tryAdd(result, result.sources.documents, sourceDescriptor(document))) {
|
|
300
|
-
if (includeSourceContent) countOmitted(result, "sources");
|
|
301
|
-
} else countOmitted(result, "sources");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const attentionItems = attention.items.filter((item) => !entityId || item.entityId === null || item.entityId === entityId);
|
|
305
|
-
for (const item of attentionItems.filter((entry) => ["promise", "blocker"].includes(entry.kind)).slice(0, 6)) {
|
|
306
|
-
if (!tryAdd(result, result.voiceBrief.activeSignals, {
|
|
307
|
-
kind: item.kind, summary: item.summary, taskId: item.taskId || null, dueAt: item.dueAt || null
|
|
308
|
-
})) countOmitted(result, "voice");
|
|
309
|
-
}
|
|
310
|
-
for (const item of attentionItems) {
|
|
311
|
-
if (!tryAdd(result, result.attention.items, item)) countOmitted(result, "attention");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
recalculateBudget(result);
|
|
315
|
-
if (result.budget.usedBytes > limit) throw new Error("briefing budget invariant failed");
|
|
316
|
-
return result;
|
|
317
|
-
}
|
|
1
|
+
import { buildCatalog, catalogForStateRoot } from "./catalog.js";
|
|
2
|
+
import { resolveContext } from "./context.js";
|
|
3
|
+
import { relationshipContext } from "./graph.js";
|
|
4
|
+
import { learningContext } from "./learning.js";
|
|
5
|
+
import { attentionContext } from "./attention.js";
|
|
6
|
+
import { taskContext } from "./coordination.js";
|
|
7
|
+
import { sharedContext } from "./sharing.js";
|
|
8
|
+
import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
|
|
9
|
+
import { gatewayRuntimeFindings, loadGatewayRuntime } from "./gateway-runtime.js";
|
|
10
|
+
import { voiceCue } from "./voice-runtime.js";
|
|
11
|
+
|
|
12
|
+
const MIN_BYTES = 4096;
|
|
13
|
+
const MAX_BYTES = 262144;
|
|
14
|
+
|
|
15
|
+
function integer(value, field, minimum, maximum) {
|
|
16
|
+
const result = Number(value);
|
|
17
|
+
if (!Number.isInteger(result) || result < minimum || result > maximum) {
|
|
18
|
+
throw new Error(`${field} must be an integer between ${minimum} and ${maximum}`);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compactBytes(value) {
|
|
24
|
+
return Buffer.byteLength(JSON.stringify(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeKey(item) {
|
|
28
|
+
return `${item.kind}\0${item.subjectId || ""}\0${item.claim.trim().toLowerCase()}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sourceDescriptor(document, content = null) {
|
|
32
|
+
return {
|
|
33
|
+
path: document.relativePath,
|
|
34
|
+
layer: document.effectiveLayer,
|
|
35
|
+
sha256: document.sha256,
|
|
36
|
+
bytes: document.bytes,
|
|
37
|
+
loaded: content !== null,
|
|
38
|
+
content,
|
|
39
|
+
authority: document.authority
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function recalculateBudget(result) {
|
|
44
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
45
|
+
const usedBytes = compactBytes(result);
|
|
46
|
+
const remainingBytes = Math.max(0, result.budget.maxBytes - usedBytes);
|
|
47
|
+
if (result.budget.usedBytes === usedBytes && result.budget.remainingBytes === remainingBytes) break;
|
|
48
|
+
result.budget.usedBytes = usedBytes;
|
|
49
|
+
result.budget.remainingBytes = remainingBytes;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tryAdd(result, collection, item) {
|
|
54
|
+
collection.push(item);
|
|
55
|
+
recalculateBudget(result);
|
|
56
|
+
// Keep a small accounting reserve so growing omission counters can never
|
|
57
|
+
// invalidate a packet after an item has been accepted.
|
|
58
|
+
if (result.budget.usedBytes <= result.budget.maxBytes - 128) return true;
|
|
59
|
+
collection.pop();
|
|
60
|
+
recalculateBudget(result);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function trySet(result, object, key, item) {
|
|
65
|
+
const previous = object[key];
|
|
66
|
+
object[key] = item;
|
|
67
|
+
recalculateBudget(result);
|
|
68
|
+
if (result.budget.usedBytes <= result.budget.maxBytes - 128) return true;
|
|
69
|
+
object[key] = previous;
|
|
70
|
+
recalculateBudget(result);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function countOmitted(result, section, count = 1) {
|
|
75
|
+
result.budget.omitted[section] += count;
|
|
76
|
+
recalculateBudget(result);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function matchesScope(item, entityId, projectId) {
|
|
80
|
+
if (!entityId && !projectId) return item.subjectId === null;
|
|
81
|
+
return item.subjectId === null || item.subjectId === entityId || item.subjectId === projectId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function taskMatchesScope(task, entityId) {
|
|
85
|
+
return !entityId || task.createdBy === entityId || task.assigneeId === entityId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function voiceProfile(entity) {
|
|
89
|
+
const value = entity?.attributes?.voice;
|
|
90
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
91
|
+
const allowed = new Set(["warmth", "directness", "humor", "length", "rhythm", "formality"]);
|
|
92
|
+
return Object.fromEntries(Object.entries(value)
|
|
93
|
+
.filter(([key, item]) => allowed.has(key) && ["string", "number", "boolean"].includes(typeof item))
|
|
94
|
+
.map(([key, item]) => [key, typeof item === "string" ? item.slice(0, 120) : item]));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function settleReads(promises) {
|
|
98
|
+
const settled = await Promise.allSettled(promises);
|
|
99
|
+
const failed = settled.find((item) => item.status === "rejected");
|
|
100
|
+
if (failed) throw failed.reason;
|
|
101
|
+
return settled.map((item) => item.value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Assemble one immutable, privacy-filtered context packet for a host session.
|
|
106
|
+
* The packet is descriptive only and fits maxBytes as compact UTF-8 JSON.
|
|
107
|
+
*/
|
|
108
|
+
export async function sessionBriefing({
|
|
109
|
+
root = process.cwd(), cwd = root, host = "generic", entityId = null,
|
|
110
|
+
userId = null, tenantId = null, groupId = null, projectId = null, currentTaskId = null,
|
|
111
|
+
includePrivate = false, focusActive = true, includeSourceContent = true,
|
|
112
|
+
maxBytes = 16384, now = new Date(), catalog: providedCatalog = null, userStateRoot = null,
|
|
113
|
+
sourceDiagnostics = null, prompt = null
|
|
114
|
+
} = {}) {
|
|
115
|
+
const limit = integer(maxBytes, "maxBytes", MIN_BYTES, MAX_BYTES);
|
|
116
|
+
if (!new Set(["codex", "claude", "generic"]).has(host)) throw new Error(`unsupported host: ${host}`);
|
|
117
|
+
if (groupId !== null && includePrivate) throw new Error("private context cannot be assembled for a group audience");
|
|
118
|
+
|
|
119
|
+
const catalog = providedCatalog || await buildCatalog(root);
|
|
120
|
+
const sources = await resolveContext({
|
|
121
|
+
root: catalog.root, cwd, host, maxBytes: limit,
|
|
122
|
+
includeContent: includeSourceContent && groupId === null,
|
|
123
|
+
catalog
|
|
124
|
+
});
|
|
125
|
+
const portableRelationship = Boolean(entityId && userStateRoot && userStateRoot !== catalog.root);
|
|
126
|
+
const userCatalog = userStateRoot && userStateRoot !== catalog.root
|
|
127
|
+
? catalogForStateRoot(catalog, userStateRoot) : catalog;
|
|
128
|
+
const learningScope = {
|
|
129
|
+
personaId: entityId, userId, tenantId, projectId, groupId, taskId: currentTaskId
|
|
130
|
+
};
|
|
131
|
+
const relationship = entityId
|
|
132
|
+
? await relationshipContext(userStateRoot && userStateRoot !== catalog.root
|
|
133
|
+
? { root: userStateRoot, entityId, includePrivate, groupId, catalog: userCatalog }
|
|
134
|
+
: { root: catalog.root, entityId, includePrivate, groupId, catalog })
|
|
135
|
+
: null;
|
|
136
|
+
const [learned, attention, tasks, shared, userLearned, personas, gateway] = await settleReads([
|
|
137
|
+
learningContext({ root: catalog.root, includePrivate, groupId, scope: learningScope, maxItems: 50, catalog, now }),
|
|
138
|
+
attentionContext({
|
|
139
|
+
root: catalog.root, includePrivate, entityId, groupId, projectId, currentTaskId,
|
|
140
|
+
focusActive, markPresented: false, maxItems: 20, now, catalog
|
|
141
|
+
}),
|
|
142
|
+
taskContext({ root: catalog.root, includePrivate, groupId, projectId, includeClosed: false, maxItems: 100, catalog }),
|
|
143
|
+
sharedContext({ root: catalog.root, includePrivate, groupId, maxItems: 50, catalog }),
|
|
144
|
+
userStateRoot && userStateRoot !== catalog.root
|
|
145
|
+
? learningContext({ root: userStateRoot, includePrivate, groupId, scope: learningScope, maxItems: 50, now,
|
|
146
|
+
catalog: userCatalog })
|
|
147
|
+
: Promise.resolve({ items: [] }),
|
|
148
|
+
loadPersonaRuntime(catalog.root, catalog),
|
|
149
|
+
loadGatewayRuntime(catalog.root, catalog)
|
|
150
|
+
]);
|
|
151
|
+
const personaFindings = personaRuntimeFindings(personas.policy, personas.runtime);
|
|
152
|
+
if (personaFindings.length) throw new Error(`persona runtime failed closed: ${personaFindings.join(", ")}`);
|
|
153
|
+
const registeredPersona = entityId ? personas.runtime.personas.find((item) => item.personaId === entityId) : null;
|
|
154
|
+
if (registeredPersona && registeredPersona.status !== "active") throw new Error(`persona is not active in the authenticated roster: ${entityId}`);
|
|
155
|
+
if (registeredPersona && groupId !== null && registeredPersona.groupId !== groupId) {
|
|
156
|
+
throw new Error(`persona is not visible in the exact group scope: ${entityId}`);
|
|
157
|
+
}
|
|
158
|
+
const gatewayFindings = gatewayRuntimeFindings(gateway.policy, gateway.runtime);
|
|
159
|
+
if (gatewayFindings.length) throw new Error(`gateway runtime failed closed: ${gatewayFindings.join(", ")}`);
|
|
160
|
+
|
|
161
|
+
const result = {
|
|
162
|
+
schema: "agentspine.session-briefing/v1",
|
|
163
|
+
root: catalog.root,
|
|
164
|
+
cwd: sources.cwd,
|
|
165
|
+
host,
|
|
166
|
+
scope: { entityId, userId, tenantId, groupId, projectId, includePrivate },
|
|
167
|
+
focus: { active: Boolean(focusActive), currentTaskId },
|
|
168
|
+
sources: { documents: [], diagnostics: sourceDiagnostics },
|
|
169
|
+
tasks: [],
|
|
170
|
+
relationship: relationship ? {
|
|
171
|
+
status: relationship.status || "loaded",
|
|
172
|
+
reason: relationship.reason || null,
|
|
173
|
+
entity: null,
|
|
174
|
+
relatedEntities: [],
|
|
175
|
+
edges: []
|
|
176
|
+
} : null,
|
|
177
|
+
voiceBrief: {
|
|
178
|
+
schema: "agentspine.voice-brief/v1",
|
|
179
|
+
personaId: entityId,
|
|
180
|
+
displayName: null,
|
|
181
|
+
language: null,
|
|
182
|
+
profile: {},
|
|
183
|
+
personaSources: [],
|
|
184
|
+
preferences: [],
|
|
185
|
+
corrections: [],
|
|
186
|
+
noGos: [],
|
|
187
|
+
currentTask: null,
|
|
188
|
+
currentGoal: null,
|
|
189
|
+
activeSignals: [],
|
|
190
|
+
cue: voiceCue(prompt),
|
|
191
|
+
guidance: [
|
|
192
|
+
"Lead with the useful outcome.",
|
|
193
|
+
"Use the active persona naturally; do not imitate emotion or claim consciousness.",
|
|
194
|
+
"Do not ask again for facts already present in current scoped context.",
|
|
195
|
+
"Acknowledge current frustration, uncertainty, correction, or success briefly when relevant, then act.",
|
|
196
|
+
"Be honest about uncertainty and take responsibility for concrete mistakes."
|
|
197
|
+
],
|
|
198
|
+
authority: "context-only"
|
|
199
|
+
},
|
|
200
|
+
learning: [],
|
|
201
|
+
shared: [],
|
|
202
|
+
attention: { suppressed: attention.suppressed, items: [] },
|
|
203
|
+
budget: {
|
|
204
|
+
maxBytes: limit,
|
|
205
|
+
usedBytes: 0,
|
|
206
|
+
remainingBytes: 0,
|
|
207
|
+
measurement: "compact-json-utf8",
|
|
208
|
+
omitted: { sources: 0, tasks: 0, relationships: 0, voice: 0, learning: 0, shared: 0, attention: 0 }
|
|
209
|
+
},
|
|
210
|
+
authority: "context-only",
|
|
211
|
+
note: "This packet is descriptive context only. It grants no delegation, host, tool, file, network, production, spending, or policy rights. Native host rules and explicit local policy remain authoritative."
|
|
212
|
+
};
|
|
213
|
+
recalculateBudget(result);
|
|
214
|
+
if (result.budget.usedBytes > limit) throw new Error(`maxBytes is too small for the briefing envelope; use at least ${MIN_BYTES}`);
|
|
215
|
+
|
|
216
|
+
const scopedTasks = tasks.items.filter((task) => taskMatchesScope(task, entityId));
|
|
217
|
+
if (currentTaskId && !scopedTasks.some((task) => task.id === currentTaskId)) {
|
|
218
|
+
throw new Error(`current task is not visible in this briefing scope: ${currentTaskId}`);
|
|
219
|
+
}
|
|
220
|
+
const orderedTasks = [...scopedTasks].sort((left, right) => {
|
|
221
|
+
if (left.id === currentTaskId) return -1;
|
|
222
|
+
if (right.id === currentTaskId) return 1;
|
|
223
|
+
return right.priority - left.priority || left.id.localeCompare(right.id);
|
|
224
|
+
});
|
|
225
|
+
for (const task of orderedTasks) {
|
|
226
|
+
if (!tryAdd(result, result.tasks, task)) countOmitted(result, "tasks");
|
|
227
|
+
}
|
|
228
|
+
const focusedTask = orderedTasks.find((task) => task.id === currentTaskId) || orderedTasks[0] || null;
|
|
229
|
+
if (focusedTask && !trySet(result, result.voiceBrief, "currentTask", {
|
|
230
|
+
id: focusedTask.id, title: focusedTask.title, status: focusedTask.status
|
|
231
|
+
})) countOmitted(result, "voice");
|
|
232
|
+
const focusedGoal = entityId ? gateway.policy.goals.find((goal) => goal.agentId === entityId && goal.status === "active"
|
|
233
|
+
&& (projectId === null || goal.projectId === projectId) && goal.groupId === groupId) : null;
|
|
234
|
+
if (focusedGoal && !trySet(result, result.voiceBrief, "currentGoal", {
|
|
235
|
+
goalId: focusedGoal.goalId, successCriterion: focusedGoal.successCriterion,
|
|
236
|
+
nextSafeStep: focusedGoal.nextSafeStep, deadline: focusedGoal.deadline,
|
|
237
|
+
heartbeatAt: focusedGoal.heartbeatAt, blocker: focusedGoal.blocker
|
|
238
|
+
})) countOmitted(result, "voice");
|
|
239
|
+
|
|
240
|
+
if (registeredPersona) {
|
|
241
|
+
if (!trySet(result, result.voiceBrief, "displayName", registeredPersona.displayName)) countOmitted(result, "voice");
|
|
242
|
+
const binding = personas.policy.bindings.find((item) => item.id === registeredPersona.bindingId && item.active);
|
|
243
|
+
if (binding?.sourceBinding && !tryAdd(result, result.voiceBrief.personaSources, binding.sourceBinding)) countOmitted(result, "voice");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (relationship && relationship.status !== "degraded") {
|
|
247
|
+
if (!trySet(result, result.relationship, "entity", relationship.entity)) countOmitted(result, "relationships");
|
|
248
|
+
if (!registeredPersona && !trySet(result, result.voiceBrief, "displayName", relationship.entity.displayName || null)) countOmitted(result, "voice");
|
|
249
|
+
const language = typeof relationship.entity.attributes?.language === "string"
|
|
250
|
+
? relationship.entity.attributes.language.slice(0, 40) : null;
|
|
251
|
+
if (!trySet(result, result.voiceBrief, "language", language)) countOmitted(result, "voice");
|
|
252
|
+
if (!trySet(result, result.voiceBrief, "profile", voiceProfile(relationship.entity))) countOmitted(result, "voice");
|
|
253
|
+
if (!portableRelationship) {
|
|
254
|
+
for (const entity of relationship.relatedEntities.filter((item) => item.id !== entityId)) {
|
|
255
|
+
if (!tryAdd(result, result.relationship.relatedEntities, entity)) countOmitted(result, "relationships");
|
|
256
|
+
}
|
|
257
|
+
for (const edge of relationship.edges) {
|
|
258
|
+
if (!tryAdd(result, result.relationship.edges, edge)) countOmitted(result, "relationships");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const portableKinds = new Set(["preference", "no-go", "correction", "reference"]);
|
|
264
|
+
const portableItems = userLearned.items.filter((item) => portableKinds.has(item.kind) && matchesScope(item, entityId, null));
|
|
265
|
+
const localItems = [...portableItems, ...learned.items.filter((item) => matchesScope(item, entityId, projectId))];
|
|
266
|
+
const voiceCollections = {
|
|
267
|
+
preference: result.voiceBrief.preferences,
|
|
268
|
+
correction: result.voiceBrief.corrections,
|
|
269
|
+
"no-go": result.voiceBrief.noGos
|
|
270
|
+
};
|
|
271
|
+
for (const item of localItems.filter((entry) => voiceCollections[entry.kind]).slice(0, 12)) {
|
|
272
|
+
if (!tryAdd(result, voiceCollections[item.kind], item.claim)) countOmitted(result, "voice");
|
|
273
|
+
}
|
|
274
|
+
const localKeys = new Set(localItems.map(normalizeKey));
|
|
275
|
+
for (const item of localItems) {
|
|
276
|
+
if (!tryAdd(result, result.learning, item)) countOmitted(result, "learning");
|
|
277
|
+
}
|
|
278
|
+
for (const item of shared.items.filter((entry) => matchesScope(entry, entityId, projectId))) {
|
|
279
|
+
if (localKeys.has(normalizeKey(item))) {
|
|
280
|
+
countOmitted(result, "shared");
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (!tryAdd(result, result.shared, item)) countOmitted(result, "shared");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const sourceByteCap = Math.min(8192, Math.floor(limit / 2));
|
|
287
|
+
let sourceBytes = 0;
|
|
288
|
+
const personaLayers = new Set(["soul", "identity", "voice", "conduct"]);
|
|
289
|
+
for (const document of sources.documents.filter((item) => personaLayers.has(item.effectiveLayer))) {
|
|
290
|
+
if (!tryAdd(result, result.voiceBrief.personaSources, document.relativePath)) countOmitted(result, "voice");
|
|
291
|
+
}
|
|
292
|
+
for (const document of sources.documents) {
|
|
293
|
+
const mayLoad = document.loaded && document.content !== null && sourceBytes + document.bytes <= sourceByteCap;
|
|
294
|
+
const loaded = mayLoad ? sourceDescriptor(document, document.content) : null;
|
|
295
|
+
if (loaded && tryAdd(result, result.sources.documents, loaded)) {
|
|
296
|
+
sourceBytes += document.bytes;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (tryAdd(result, result.sources.documents, sourceDescriptor(document))) {
|
|
300
|
+
if (includeSourceContent) countOmitted(result, "sources");
|
|
301
|
+
} else countOmitted(result, "sources");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const attentionItems = attention.items.filter((item) => !entityId || item.entityId === null || item.entityId === entityId);
|
|
305
|
+
for (const item of attentionItems.filter((entry) => ["promise", "blocker"].includes(entry.kind)).slice(0, 6)) {
|
|
306
|
+
if (!tryAdd(result, result.voiceBrief.activeSignals, {
|
|
307
|
+
kind: item.kind, summary: item.summary, taskId: item.taskId || null, dueAt: item.dueAt || null
|
|
308
|
+
})) countOmitted(result, "voice");
|
|
309
|
+
}
|
|
310
|
+
for (const item of attentionItems) {
|
|
311
|
+
if (!tryAdd(result, result.attention.items, item)) countOmitted(result, "attention");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
recalculateBudget(result);
|
|
315
|
+
if (result.budget.usedBytes > limit) throw new Error("briefing budget invariant failed");
|
|
316
|
+
return result;
|
|
317
|
+
}
|