@dsh-enhanced/assistant-growth-driver 0.1.66 → 0.1.67
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/README.md +51 -8
- package/lib/config.d.ts +12 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +6 -6
- package/lib/config.js.map +1 -1
- package/lib/growth-agent.d.ts +62 -0
- package/lib/growth-agent.d.ts.map +1 -1
- package/lib/growth-agent.js +218 -13
- package/lib/growth-agent.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +8 -18
- package/lib/index.js.map +1 -1
- package/lib/source-port.d.ts.map +1 -1
- package/lib/source-port.js +0 -1
- package/lib/source-port.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +14 -19
package/lib/growth-agent.js
CHANGED
|
@@ -4,7 +4,7 @@ import { anySourceGrowthRunDigest, validateWorkItemSourceGrowthRunBinding, sourc
|
|
|
4
4
|
import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
5
5
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
6
6
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
7
|
-
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
7
|
+
import { defineTool, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools';
|
|
8
8
|
import { workItemRunSource } from './work-item-source.js';
|
|
9
9
|
import { GROWTH_PROTECTED_PLUGIN_DENYLIST, } from './source-port.js';
|
|
10
10
|
import { resolveSourcePreparation } from './source-edits.js';
|
|
@@ -872,7 +872,131 @@ function registerGrowthTools(agent, input, sourcePlane, creationNamespace, revis
|
|
|
872
872
|
}
|
|
873
873
|
agentCtx.effect(() => () => disposers.forEach(dispose => dispose()), 'assistant-growth-driver.realm-tools');
|
|
874
874
|
}
|
|
875
|
-
|
|
875
|
+
/** Extract only the program source from one raw `tool/call` arguments JSON string. */
|
|
876
|
+
function runCodeSource(raw) {
|
|
877
|
+
if (typeof raw !== 'string')
|
|
878
|
+
return undefined;
|
|
879
|
+
try {
|
|
880
|
+
const parsed = JSON.parse(raw);
|
|
881
|
+
return typeof parsed.code === 'string' ? parsed.code : undefined;
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const eventId = (value) => typeof value === 'string' ? value : typeof value === 'number' && Number.isFinite(value) ? String(value) : undefined;
|
|
888
|
+
/**
|
|
889
|
+
* Project a bounded structural trajectory from the session's PTC log events.
|
|
890
|
+
* Deliberately reads only names, ids, times, error flags and byte lengths: the
|
|
891
|
+
* nested `arguments` and `content` carried by `tool/ptc-dispatch`/`-start`
|
|
892
|
+
* and the program source in `tool/call` never survive into the returned
|
|
893
|
+
* object, and unknown/malformed records are skipped rather than trusted.
|
|
894
|
+
* Bounded by the frozen tool allowlist and by plain integer counters.
|
|
895
|
+
*/
|
|
896
|
+
export function ptcTrajectory(events, admittedToolNames) {
|
|
897
|
+
let runCodeCalls = 0, failedRunCodeCalls = 0, scriptsHashed = 0, scriptBytes = 0;
|
|
898
|
+
let nestedCalls = 0, failedNestedCalls = 0, unadmittedNestedCalls = 0;
|
|
899
|
+
let longestSubCallMs = 0, longestRunCodeMs = 0;
|
|
900
|
+
const scriptHash = createHash('sha256');
|
|
901
|
+
const runCodeStarts = new Map();
|
|
902
|
+
const subCallStarts = new Map();
|
|
903
|
+
const byName = new Map();
|
|
904
|
+
const timeOf = (event) => typeof event.time === 'number' && Number.isFinite(event.time) ? event.time : undefined;
|
|
905
|
+
for (const raw of events) {
|
|
906
|
+
if (raw === null || typeof raw !== 'object')
|
|
907
|
+
continue;
|
|
908
|
+
const data = raw.data;
|
|
909
|
+
if (data === undefined || data === null || typeof data !== 'object')
|
|
910
|
+
continue;
|
|
911
|
+
const time = timeOf(raw);
|
|
912
|
+
if (raw.type === 'tool/call') {
|
|
913
|
+
if (data.name !== RUN_CODE_NAME)
|
|
914
|
+
continue;
|
|
915
|
+
runCodeCalls += 1;
|
|
916
|
+
const callId = eventId(data.callId);
|
|
917
|
+
if (callId !== undefined && time !== undefined)
|
|
918
|
+
runCodeStarts.set(callId, time);
|
|
919
|
+
const code = runCodeSource(data.arguments);
|
|
920
|
+
if (code !== undefined) {
|
|
921
|
+
const bytes = Buffer.byteLength(code, 'utf8');
|
|
922
|
+
if (Number.isSafeInteger(scriptBytes + bytes))
|
|
923
|
+
scriptBytes += bytes;
|
|
924
|
+
// Length-prefix keeps the aggregate digest unambiguous across scripts.
|
|
925
|
+
scriptHash.update(`${bytes}\0`);
|
|
926
|
+
scriptHash.update(code);
|
|
927
|
+
scriptsHashed += 1;
|
|
928
|
+
}
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (raw.type === 'tool/result') {
|
|
932
|
+
// ToolResultMessage carries the call identity as `toolCallId`; older
|
|
933
|
+
// projections used `callId`, so accept either without trusting anything
|
|
934
|
+
// else from the message.
|
|
935
|
+
const message = data.message;
|
|
936
|
+
if (message === undefined || message === null || typeof message !== 'object')
|
|
937
|
+
continue;
|
|
938
|
+
const callId = eventId(message.toolCallId) ?? eventId(message.callId);
|
|
939
|
+
const start = callId === undefined ? undefined : runCodeStarts.get(callId);
|
|
940
|
+
if (callId === undefined || start === undefined)
|
|
941
|
+
continue;
|
|
942
|
+
runCodeStarts.delete(callId);
|
|
943
|
+
if (message.isError === true)
|
|
944
|
+
failedRunCodeCalls += 1;
|
|
945
|
+
if (time !== undefined)
|
|
946
|
+
longestRunCodeMs = Math.max(longestRunCodeMs, Math.max(0, time - start));
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (raw.type === 'tool/ptc-dispatch-start') {
|
|
950
|
+
const subCallId = eventId(data.subCallId);
|
|
951
|
+
if (subCallId !== undefined && time !== undefined)
|
|
952
|
+
subCallStarts.set(subCallId, time);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (raw.type === 'tool/ptc-dispatch') {
|
|
956
|
+
nestedCalls += 1;
|
|
957
|
+
const name = typeof data.name === 'string' ? data.name : undefined;
|
|
958
|
+
if (name !== undefined && admittedToolNames.has(name))
|
|
959
|
+
byName.set(name, (byName.get(name) ?? 0) + 1);
|
|
960
|
+
else
|
|
961
|
+
unadmittedNestedCalls += 1;
|
|
962
|
+
if (data.isError === true)
|
|
963
|
+
failedNestedCalls += 1;
|
|
964
|
+
const subCallId = eventId(data.subCallId);
|
|
965
|
+
const start = subCallId === undefined ? undefined : subCallStarts.get(subCallId);
|
|
966
|
+
if (subCallId !== undefined && start !== undefined) {
|
|
967
|
+
subCallStarts.delete(subCallId);
|
|
968
|
+
if (time !== undefined)
|
|
969
|
+
longestSubCallMs = Math.max(longestSubCallMs, Math.max(0, time - start));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return Object.freeze({
|
|
974
|
+
protocol: 'assistant-growth/ptc-trajectory/v1',
|
|
975
|
+
runCodeCalls, failedRunCodeCalls, scriptBytes,
|
|
976
|
+
scriptDigest: scriptsHashed > 0 ? scriptHash.digest('hex') : null,
|
|
977
|
+
nestedCalls, failedNestedCalls, unadmittedNestedCalls,
|
|
978
|
+
toolCallsByName: Object.freeze(Object.fromEntries([...byName.entries()].sort(([left], [right]) => left.localeCompare(right)))),
|
|
979
|
+
longestSubCallMs, longestRunCodeMs,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Split the registry's presentation-agnostic visibility view into the frozen
|
|
984
|
+
* allowlist surface and the reserved PTC transport. `schemas(scope)` stays
|
|
985
|
+
* presentation-agnostic for everything the growth agent registered, but under
|
|
986
|
+
* `ptc` the Host registry appends its own reserved `run_code` definition.
|
|
987
|
+
* Keeping the split in one pure helper means the contract digest can never
|
|
988
|
+
* accidentally include the presentation transport (native and PTC runs of the
|
|
989
|
+
* same configuration persist the identical tool contract digest) while the
|
|
990
|
+
* fail-closed surface check can still require exactly one reserved transport.
|
|
991
|
+
*/
|
|
992
|
+
export function splitGrowthToolSurface(schemas) {
|
|
993
|
+
const surface = [];
|
|
994
|
+
const reserved = [];
|
|
995
|
+
for (const schema of schemas)
|
|
996
|
+
(schema.name === RUN_CODE_NAME ? reserved : surface).push(schema);
|
|
997
|
+
return { surface, reserved };
|
|
998
|
+
}
|
|
999
|
+
function summarize(events, signal, modelCalls, toolCalls, presentation, admittedToolNames) {
|
|
876
1000
|
let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0, reasoningTokens = 0;
|
|
877
1001
|
let output = '';
|
|
878
1002
|
let outcome = 'unknown';
|
|
@@ -912,7 +1036,10 @@ function summarize(events, signal, modelCalls, toolCalls) {
|
|
|
912
1036
|
outcome = 'failed';
|
|
913
1037
|
}
|
|
914
1038
|
}
|
|
915
|
-
return { outcome, output, usage: { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, modelCalls, toolCalls
|
|
1039
|
+
return { outcome, output, usage: { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, modelCalls, toolCalls,
|
|
1040
|
+
// Native wakes keep exactly the previous usage shape; the PTC block is
|
|
1041
|
+
// opt-in and structural only.
|
|
1042
|
+
...(presentation === 'ptc' ? { ptc: ptcTrajectory(events, admittedToolNames) } : {}) } };
|
|
916
1043
|
}
|
|
917
1044
|
/**
|
|
918
1045
|
* Create, run to idle and dispose exactly one bounded growth agent. Never
|
|
@@ -924,6 +1051,25 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
924
1051
|
if (workItemSource)
|
|
925
1052
|
input = { ...input, config: { ...input.config, pluginSourceProposals: { ...input.config.pluginSourceProposals, allowModification: false, allowRevision: false } } };
|
|
926
1053
|
const { authority, config, model } = input;
|
|
1054
|
+
const presentation = config.toolPresentation;
|
|
1055
|
+
if (presentation === 'ptc') {
|
|
1056
|
+
// The WorkItem author lane freezes a four-tool preauthorization set and its
|
|
1057
|
+
// own execution contract; the reserved transport is not part of it, so a
|
|
1058
|
+
// PTC wake there would fail mid-run with a misleading authorization error.
|
|
1059
|
+
// Reject the combination before any Agent is created and tell the owner why.
|
|
1060
|
+
if (workItemSource) {
|
|
1061
|
+
throw new Error('assistant-growth-driver: PTC presentation is not supported for the WorkItem author lane yet: '
|
|
1062
|
+
+ 'the reserved run_code transport is not in its frozen preauthorization; '
|
|
1063
|
+
+ 'use toolPresentation "native" for workItemFailures or wait for a Host-owned preauthorization');
|
|
1064
|
+
}
|
|
1065
|
+
// Fail closed and loudly, never fall back to native: the model surface is
|
|
1066
|
+
// an owner-declared contract, and a profile that opted into PTC but does not
|
|
1067
|
+
// mount the runtime seam cannot honestly honor it. No Agent is created.
|
|
1068
|
+
if (ctx.get('ptcRuntime', false) === undefined) {
|
|
1069
|
+
throw new Error('assistant-growth-driver: toolPresentation "ptc" requires a mounted ctx.ptcRuntime implementation '
|
|
1070
|
+
+ '(e.g. @deepseek-ai/dsh-ptc-runtime-node); load a PTC runtime in this profile or set toolPresentation to "native"');
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
927
1073
|
const sourcePlane = config.pluginSourceProposals.enabled ? input.sourcePlane : undefined;
|
|
928
1074
|
let creationNamespace;
|
|
929
1075
|
if (sourcePlane !== undefined && config.pluginSourceProposals.allowCreation
|
|
@@ -1089,12 +1235,44 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
1089
1235
|
const denied = globalNames.filter(name => !allowedTools.has(name));
|
|
1090
1236
|
if (denied.length > 0)
|
|
1091
1237
|
agentCtx.tools.restrict({ deny: denied });
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1238
|
+
if (presentation === 'ptc') {
|
|
1239
|
+
// PTC is presentation only. The single reserved `run_code` transport is
|
|
1240
|
+
// appended by the Host registry through this scoped presentAs() effect
|
|
1241
|
+
// (an effect owned by the Agent realm, released with it); the driver
|
|
1242
|
+
// never registers, wraps or overrides that reserved name. Refuse any
|
|
1243
|
+
// pre-existing definition before the declaration so a forged or
|
|
1244
|
+
// duplicate transport can never be mistaken for the Host one.
|
|
1245
|
+
if (typeof agentCtx.tools.presentAs !== 'function') {
|
|
1246
|
+
throw new Error('assistant-growth-driver: toolPresentation "ptc" requires a Host tool registry with presentAs()');
|
|
1247
|
+
}
|
|
1248
|
+
if (agentCtx.tools.schemas(agent).some(schema => schema.name === RUN_CODE_NAME)) {
|
|
1249
|
+
throw new Error('assistant-growth-driver: reserved run_code transport is already visible before PTC presentation');
|
|
1250
|
+
}
|
|
1251
|
+
agentCtx.tools.presentAs('ptc');
|
|
1252
|
+
const reserved = agentCtx.tools.get(RUN_CODE_NAME, agent);
|
|
1253
|
+
if (reserved === undefined || reserved.name !== RUN_CODE_NAME || agentCtx.tools.get(RUN_CODE_NAME, agent) !== reserved) {
|
|
1254
|
+
throw new Error('assistant-growth-driver: PTC growth Agent does not expose the Host reserved run_code transport');
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const finalSchemas = agentCtx.tools.schemas(agent);
|
|
1258
|
+
const { surface: allowlistedSchemas, reserved: reservedSchemas } = splitGrowthToolSurface(finalSchemas);
|
|
1259
|
+
const outsideAllowlist = allowlistedSchemas.filter(schema => !allowedTools.has(schema.name)).map(schema => schema.name);
|
|
1260
|
+
const missing = [...allowedTools].filter(name => !allowlistedSchemas.some(schema => schema.name === name));
|
|
1261
|
+
// Exactly the frozen allowlist plus, only under PTC, exactly one reserved
|
|
1262
|
+
// transport definition. Missing, extra, duplicated or foreign entries all
|
|
1263
|
+
// fail closed before any model request.
|
|
1264
|
+
if (outsideAllowlist.length > 0 || missing.length > 0
|
|
1265
|
+
|| reservedSchemas.length !== (presentation === 'ptc' ? 1 : 0)
|
|
1266
|
+
|| reservedSchemas.some(schema => schema.name !== RUN_CODE_NAME)) {
|
|
1267
|
+
throw new Error(`assistant-growth-driver: growth Agent tool surface is not exactly the frozen allowlist: ${[...outsideAllowlist, ...missing].join(', ')}`);
|
|
1096
1268
|
}
|
|
1097
|
-
|
|
1269
|
+
// Contract digest: allowlist-only, so the same configuration persists the
|
|
1270
|
+
// identical tool contract digest under native and PTC presentation.
|
|
1271
|
+
const pinnedDigest = acceptanceDigest([...allowlistedSchemas].sort((a, b) => a.name.localeCompare(b.name)));
|
|
1272
|
+
// Model-visible wire digest, deliberately separate from pinnedDigest:
|
|
1273
|
+
// native presents the allowlist surface, PTC presents only the reserved
|
|
1274
|
+
// transport. Recomputed from the filtered assembly before every request.
|
|
1275
|
+
let wireDigest;
|
|
1098
1276
|
const session = agent.session;
|
|
1099
1277
|
const definitions = new Map();
|
|
1100
1278
|
let binding;
|
|
@@ -1104,7 +1282,9 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
1104
1282
|
|| agent.session !== session || String(session.id) !== String(sessionId) || agent.ctx !== agentCtx
|
|
1105
1283
|
|| session.header.cwd !== authority.scope.workspace || session.header.agentPreset !== authority.scope.preset
|
|
1106
1284
|
|| preauthorizationRemovers.length !== 4 || anySourceGrowthRunDigest(binding) !== bindingDigest
|
|
1107
|
-
|
|
1285
|
+
// Digest the allowlist surface only: the PTC presentation transport is
|
|
1286
|
+
// never part of the frozen tool contract.
|
|
1287
|
+
|| acceptanceDigest([...splitGrowthToolSurface(agentCtx.tools.schemas(agent)).surface].sort((a, b) => a.name.localeCompare(b.name))) !== pinnedDigest
|
|
1108
1288
|
|| WORK_ITEM_SOURCE_TOOL_NAMES.some(name => agentCtx.tools.get(name, agent) !== definitions.get(name)))
|
|
1109
1289
|
return false;
|
|
1110
1290
|
if (!execution)
|
|
@@ -1146,7 +1326,19 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
1146
1326
|
const assembly = await next();
|
|
1147
1327
|
if (context.agent !== agent)
|
|
1148
1328
|
return assembly;
|
|
1149
|
-
|
|
1329
|
+
if (presentation === 'ptc') {
|
|
1330
|
+
// The Host assembles the collapsed PTC wire surface; anything other
|
|
1331
|
+
// than exactly the reserved transport means the presentation did not
|
|
1332
|
+
// take effect and must fail closed, never leak the native surface.
|
|
1333
|
+
if (assembly.tools.length !== 1 || assembly.tools[0]?.name !== RUN_CODE_NAME) {
|
|
1334
|
+
throw new Error('assistant-growth-driver: PTC growth Agent must present exactly the reserved run_code transport');
|
|
1335
|
+
}
|
|
1336
|
+
wireDigest = acceptanceDigest(assembly.tools);
|
|
1337
|
+
return { ...assembly, tools: assembly.tools };
|
|
1338
|
+
}
|
|
1339
|
+
const tools = assembly.tools.filter(tool => allowedTools.has(tool.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
1340
|
+
wireDigest = acceptanceDigest(tools);
|
|
1341
|
+
return { ...assembly, tools };
|
|
1150
1342
|
});
|
|
1151
1343
|
agentCtx.on('llm/stream', async function* (options, next) {
|
|
1152
1344
|
if (options.sessionId !== agent.session.id) {
|
|
@@ -1161,7 +1353,9 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
1161
1353
|
if (options.provider !== model.provider || options.model !== model.model
|
|
1162
1354
|
|| model.reasoningEffort !== undefined && options.reasoningEffort !== model.reasoningEffort
|
|
1163
1355
|
|| options.maxTokens === undefined || options.maxTokens > config.maxOutputTokens
|
|
1164
|
-
|
|
1356
|
+
// The wire digest is pinned separately from the allowlist contract
|
|
1357
|
+
// digest: under PTC the request carries only the reserved transport.
|
|
1358
|
+
|| wireDigest === undefined || acceptanceDigest(options.tools ?? []) !== wireDigest
|
|
1165
1359
|
|| modelCalls >= config.maxModelCalls) {
|
|
1166
1360
|
agent.cancel({ kind: 'hook', reason: 'assistant-growth-driver-model-rejected' });
|
|
1167
1361
|
throw new Error('assistant-growth-driver: growth model request rejected by the frozen contract');
|
|
@@ -1191,7 +1385,14 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
1191
1385
|
throw error;
|
|
1192
1386
|
}
|
|
1193
1387
|
combined.throwIfAborted();
|
|
1194
|
-
|
|
1388
|
+
// The reserved PTC transport is the ONLY name outside the frozen
|
|
1389
|
+
// allowlist that may execute, and only under the declared PTC mode.
|
|
1390
|
+
// Every execution that reaches this guard - model-direct `run_code`
|
|
1391
|
+
// and each nested sub-dispatch, failed or cancelled included after
|
|
1392
|
+
// admission - counts against the same maxToolCalls budget.
|
|
1393
|
+
const admitted = allowedTools.has(execution.name)
|
|
1394
|
+
|| (presentation === 'ptc' && execution.name === RUN_CODE_NAME);
|
|
1395
|
+
if (!admitted || totalToolCalls >= config.maxToolCalls) {
|
|
1195
1396
|
agent.cancel({ kind: 'hook', reason: 'assistant-growth-driver-tool-limit' });
|
|
1196
1397
|
return 'assistant-growth-driver: tool request rejected by the frozen allowlist';
|
|
1197
1398
|
}
|
|
@@ -1238,6 +1439,10 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
1238
1439
|
executionContractDigest: acceptanceDigest({
|
|
1239
1440
|
protocol: 'assistant-growth/execution-contract/v1',
|
|
1240
1441
|
prompt: executionPrompt,
|
|
1442
|
+
// Native keeps the historical contract shape byte-for-byte; PTC
|
|
1443
|
+
// additionally freezes which presentation the wake was authorized
|
|
1444
|
+
// for. The model surface difference can never be silent.
|
|
1445
|
+
...(presentation === 'ptc' ? { toolPresentation: 'ptc' } : {}),
|
|
1241
1446
|
guardVersion: 1, toolContractDigest: pinnedDigest, model,
|
|
1242
1447
|
bounds: { maxModelCalls: config.maxModelCalls, maxToolCalls: config.maxToolCalls,
|
|
1243
1448
|
maxOutputTokens: config.maxOutputTokens, maxDurationMs: config.maxDurationMs,
|
|
@@ -1279,7 +1484,7 @@ export async function runGrowthAgent(ctx, input) {
|
|
|
1279
1484
|
source: { kind: 'assistant-growth-driver', form: 'notice', summary: 'Growth review wake' },
|
|
1280
1485
|
}));
|
|
1281
1486
|
await agent.whenIdle();
|
|
1282
|
-
const summary = summarize(agent.session.snapshotEvents(), combined, modelCalls, totalToolCalls);
|
|
1487
|
+
const summary = summarize(agent.session.snapshotEvents(), combined, modelCalls, totalToolCalls, presentation, allowedTools);
|
|
1283
1488
|
await sessions.flush(agent.session);
|
|
1284
1489
|
return { sessionId: String(sessionId), model, ...summary, sourceProposals: Object.freeze({ ...sourceCounters }) };
|
|
1285
1490
|
}
|