@sema-agent/core 5.7.0 → 5.8.0
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/CHANGELOG.md +37 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/subagent.d.ts +4 -7
- package/dist/agents/subagent.js +10 -19
- package/dist/agents/teacher.js +3 -3
- package/dist/agents/team.d.ts +1 -1
- package/dist/agents/team.js +1 -1
- package/dist/agents/verify.js +1 -1
- package/dist/brain/anthropic.js +24 -35
- package/dist/brain/openai.js +22 -33
- package/dist/brain/stream-engine.d.ts +0 -2
- package/dist/brain/stream-engine.js +5 -47
- package/dist/brain/stream-shared.d.ts +0 -10
- package/dist/brain/stream-shared.js +0 -23
- package/dist/brain/terminal-cause.d.ts +0 -1
- package/dist/brain/terminal-cause.js +0 -3
- package/dist/brain/timeout.d.ts +4 -2
- package/dist/brain/timeout.js +4 -14
- package/dist/config/catalog.d.ts +3 -11
- package/dist/config/catalog.js +30 -69
- package/dist/config/defaults.d.ts +0 -3
- package/dist/config/defaults.js +0 -3
- package/dist/core/auto-compaction.d.ts +2 -11
- package/dist/core/auto-compaction.js +3 -67
- package/dist/core/checkpoint-store.d.ts +10 -8
- package/dist/core/checkpoint-store.js +7 -7
- package/dist/core/memory-engine/file-backend.js +20 -17
- package/dist/core/memory-engine/layout.d.ts +5 -0
- package/dist/core/memory-engine/layout.js +72 -47
- package/dist/core/runner/assemble-result.d.ts +2 -8
- package/dist/core/runner/assemble-result.js +15 -16
- package/dist/core/runner/prepare-task.d.ts +26 -6
- package/dist/core/runner/prepare-task.js +203 -141
- package/dist/core/runner/runtask.d.ts +0 -2
- package/dist/core/runner/runtask.js +200 -244
- package/dist/core/session-policy-store.d.ts +5 -0
- package/dist/core/session-policy-store.js +3 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
- package/dist/core/task-registry-agent.d.ts +2 -0
- package/dist/core/task-registry-agent.js +33 -1
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/task-registry.js +9 -1
- package/dist/core/tool-errors.js +10 -2
- package/dist/core/trace.d.ts +1 -8
- package/dist/core/types.d.ts +24 -30
- package/dist/core/usage-window-store.d.ts +39 -0
- package/dist/core/usage-window-store.js +115 -0
- package/dist/engine/execution-env/node-execution-env.js +0 -22
- package/dist/engine/harness/agent-harness.d.ts +0 -7
- package/dist/engine/harness/agent-harness.js +13 -53
- package/dist/engine/harness/types.d.ts +2 -15
- package/dist/engine/llm/types.d.ts +1 -2
- package/dist/engine/loop/agent-loop.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +5 -171
- package/dist/engine/loop/types.d.ts +0 -18
- package/dist/index.d.ts +10 -5
- package/dist/index.js +8 -4
- package/dist/orchestration/goal.js +1 -5
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +1 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -6
- package/dist/orchestration/workflow-governance.js +33 -47
- package/dist/prompt-assembly/event-registry.js +2 -3
- package/dist/stores/cc/lockfile.js +9 -11
- package/dist/stores/cc/mailbox-store.js +49 -38
- package/dist/stores/file/index.d.ts +3 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/usage-window-store.d.ts +9 -0
- package/dist/stores/file/usage-window-store.js +86 -0
- package/dist/tools/fs/fs-bash.d.ts +1 -10
- package/dist/tools/fs/fs-bash.js +20 -60
- package/dist/tools/fs/index.d.ts +0 -2
- package/dist/tools/fs/index.js +1 -2
- package/package.json +1 -4
- package/dist/bin/sema-tb.d.ts +0 -31
- package/dist/bin/sema-tb.js +0 -448
- package/dist/bin/tb-env.d.ts +0 -2
- package/dist/bin/tb-env.js +0 -17
- package/dist/brain/walltime.d.ts +0 -1
- package/dist/brain/walltime.js +0 -1
- package/dist/core/runner/call-cap.d.ts +0 -67
- package/dist/core/runner/call-cap.js +0 -145
- package/dist/core/runner/cut-kill.d.ts +0 -10
- package/dist/core/runner/cut-kill.js +0 -37
|
@@ -17,8 +17,6 @@ export const WHITELIST_KEYS = [
|
|
|
17
17
|
"systemPrompt",
|
|
18
18
|
"images",
|
|
19
19
|
"limits",
|
|
20
|
-
"maxCostUsd",
|
|
21
|
-
"maxTokens",
|
|
22
20
|
];
|
|
23
21
|
const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
24
22
|
export function resolveModelName(name, allowlist, models) {
|
|
@@ -91,36 +89,26 @@ function pickWhitelist(scriptSpec) {
|
|
|
91
89
|
}
|
|
92
90
|
if (s.limits !== undefined) {
|
|
93
91
|
if (typeof s.limits !== "object" || s.limits === null || Array.isArray(s.limits)) {
|
|
94
|
-
throw new WorkflowScriptError("agent(spec): `limits` must be an object { maxTurns?,
|
|
92
|
+
throw new WorkflowScriptError("agent(spec): `limits` must be an object { maxTurns?, maxWalltimeMs?, maxTokens?, maxCostUsd? }");
|
|
95
93
|
}
|
|
96
94
|
const l = s.limits;
|
|
97
95
|
const limits = {};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
96
|
+
const readAxis = (field, what) => {
|
|
97
|
+
const raw = l[field];
|
|
98
|
+
if (raw === undefined)
|
|
99
|
+
return;
|
|
100
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
101
|
+
throw new WorkflowScriptError(`agent(spec): \`limits.${field}\` must be a number`);
|
|
102
|
+
if (raw <= 0)
|
|
103
|
+
throw new WorkflowScriptError(NON_POSITIVE_LIMIT_MSG(field, what));
|
|
104
|
+
limits[field] = raw;
|
|
105
|
+
};
|
|
106
|
+
readAxis("maxTurns", "turn cap");
|
|
107
|
+
readAxis("maxWalltimeMs", "wall-clock window");
|
|
108
|
+
readAxis("maxTokens", "token budget");
|
|
109
|
+
readAxis("maxCostUsd", "spend ceiling");
|
|
112
110
|
safe.limits = limits;
|
|
113
111
|
}
|
|
114
|
-
if (s.maxCostUsd !== undefined) {
|
|
115
|
-
if (typeof s.maxCostUsd !== "number" || !Number.isFinite(s.maxCostUsd))
|
|
116
|
-
throw new WorkflowScriptError("agent(spec): `maxCostUsd` must be a number");
|
|
117
|
-
safe.maxCostUsd = s.maxCostUsd;
|
|
118
|
-
}
|
|
119
|
-
if (s.maxTokens !== undefined) {
|
|
120
|
-
if (typeof s.maxTokens !== "number" || !Number.isFinite(s.maxTokens))
|
|
121
|
-
throw new WorkflowScriptError("agent(spec): `maxTokens` must be a number");
|
|
122
|
-
safe.maxTokens = s.maxTokens;
|
|
123
|
-
}
|
|
124
112
|
let modelName;
|
|
125
113
|
if (s.modelName !== undefined) {
|
|
126
114
|
if (typeof s.modelName !== "string")
|
|
@@ -131,37 +119,35 @@ function pickWhitelist(scriptSpec) {
|
|
|
131
119
|
}
|
|
132
120
|
function clampResourceLimits(safe, base, caps) {
|
|
133
121
|
const notes = [];
|
|
134
|
-
const requestedCost = safe.maxCostUsd;
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (cost !== requestedCost)
|
|
138
|
-
notes.push({ field: "maxCostUsd", requested: requestedCost, applied: cost });
|
|
139
|
-
safe.maxCostUsd = cost;
|
|
140
|
-
}
|
|
141
|
-
const requestedTokens = safe.maxTokens;
|
|
142
|
-
const tokens = minDefined(safe.maxTokens, base.maxTokens, caps?.childMaxTokens);
|
|
143
|
-
if (tokens !== undefined) {
|
|
144
|
-
if (tokens !== requestedTokens)
|
|
145
|
-
notes.push({ field: "maxTokens", requested: requestedTokens, applied: tokens });
|
|
146
|
-
safe.maxTokens = tokens;
|
|
147
|
-
}
|
|
148
|
-
const requestedTimeoutSec = safe.limits?.timeoutSec;
|
|
122
|
+
const requestedCost = safe.limits?.maxCostUsd;
|
|
123
|
+
const requestedTokens = safe.limits?.maxTokens;
|
|
124
|
+
const requestedWalltimeMs = safe.limits?.maxWalltimeMs;
|
|
149
125
|
const requestedMaxTurns = safe.limits?.maxTurns;
|
|
150
|
-
const
|
|
126
|
+
const cost = minDefined(safe.limits?.maxCostUsd, base.limits?.maxCostUsd, caps?.childMaxCostUsd);
|
|
127
|
+
const tokens = minDefined(safe.limits?.maxTokens, base.limits?.maxTokens, caps?.childMaxTokens);
|
|
128
|
+
const maxWalltimeMs = minPositiveDefined(safe.limits?.maxWalltimeMs, base.limits?.maxWalltimeMs, caps?.perAgentMaxWalltimeMs);
|
|
151
129
|
const maxTurns = minPositiveDefined(safe.limits?.maxTurns, base.limits?.maxTurns, caps?.childMaxTurns);
|
|
152
|
-
if (
|
|
153
|
-
notes.push({ field: "
|
|
130
|
+
if (cost !== undefined && cost !== requestedCost)
|
|
131
|
+
notes.push({ field: "maxCostUsd", requested: requestedCost, applied: cost });
|
|
132
|
+
if (tokens !== undefined && tokens !== requestedTokens)
|
|
133
|
+
notes.push({ field: "maxTokens", requested: requestedTokens, applied: tokens });
|
|
134
|
+
if (maxWalltimeMs !== undefined && maxWalltimeMs !== requestedWalltimeMs) {
|
|
135
|
+
notes.push({ field: "maxWalltimeMs", requested: requestedWalltimeMs, applied: maxWalltimeMs });
|
|
154
136
|
}
|
|
155
137
|
if (maxTurns !== undefined && maxTurns !== requestedMaxTurns) {
|
|
156
138
|
notes.push({ field: "maxTurns", requested: requestedMaxTurns, applied: maxTurns });
|
|
157
139
|
}
|
|
158
140
|
const baseOtherLimits = { ...base.limits };
|
|
159
141
|
delete baseOtherLimits.maxTurns;
|
|
160
|
-
delete baseOtherLimits.
|
|
142
|
+
delete baseOtherLimits.maxWalltimeMs;
|
|
143
|
+
delete baseOtherLimits.maxTokens;
|
|
144
|
+
delete baseOtherLimits.maxCostUsd;
|
|
161
145
|
const rebuilt = {
|
|
162
146
|
...baseOtherLimits,
|
|
163
147
|
...(maxTurns !== undefined ? { maxTurns } : {}),
|
|
164
|
-
...(
|
|
148
|
+
...(maxWalltimeMs !== undefined ? { maxWalltimeMs } : {}),
|
|
149
|
+
...(tokens !== undefined ? { maxTokens: tokens } : {}),
|
|
150
|
+
...(cost !== undefined ? { maxCostUsd: cost } : {}),
|
|
165
151
|
};
|
|
166
152
|
if (base.limits !== undefined || Object.keys(rebuilt).length > 0)
|
|
167
153
|
safe.limits = rebuilt;
|
|
@@ -13,9 +13,8 @@ export const EVENT_PROMPT_REGISTRY = new Map([
|
|
|
13
13
|
{ kind: "skills_listing", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderSkillsListingDelta" },
|
|
14
14
|
{ kind: "mcp_instructions", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderMcpInstructionsDelta" },
|
|
15
15
|
{ kind: "mcp_dropped_tools", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderMcpDroppedTools" },
|
|
16
|
-
{ kind: "
|
|
17
|
-
{ kind: "
|
|
18
|
-
{ kind: "deadline_finalize", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#buildFinalizeText" },
|
|
16
|
+
{ kind: "limit_approach_converge", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#limitApproachFrames" },
|
|
17
|
+
{ kind: "limit_approach_deliver", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#limitApproachFrames" },
|
|
19
18
|
{ kind: "final_verification", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#final-verification" },
|
|
20
19
|
].map((e) => [e.kind, e]));
|
|
21
20
|
export function eventDefaultOn(kind) {
|
|
@@ -20,6 +20,9 @@ export async function acquireCcLock(target, opts) {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
for (let attempt = 0;; attempt++) {
|
|
23
|
+
if (attempt >= RETRIES) {
|
|
24
|
+
throw new Error(`cc lockfile: could not acquire ${lockPath} after ${RETRIES} retries`);
|
|
25
|
+
}
|
|
23
26
|
try {
|
|
24
27
|
mkdirSync(lockPath);
|
|
25
28
|
writeFileSync(ownerPath, nonce);
|
|
@@ -60,21 +63,16 @@ export async function acquireCcLock(target, opts) {
|
|
|
60
63
|
const st = statSync(lockPath);
|
|
61
64
|
if (Date.now() - st.mtimeMs > staleMs) {
|
|
62
65
|
const st2 = statSync(lockPath);
|
|
63
|
-
if (st2.mtimeMs
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
if (st2.mtimeMs === st.mtimeMs) {
|
|
67
|
+
try {
|
|
68
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
}
|
|
69
72
|
}
|
|
70
|
-
continue;
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
catch {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (attempt >= RETRIES) {
|
|
77
|
-
throw new Error(`cc lockfile: could not acquire ${lockPath} after ${RETRIES} retries (held and fresh)`);
|
|
78
76
|
}
|
|
79
77
|
const backoff = Math.min(MAX_TIMEOUT_MS, MIN_TIMEOUT_MS * 2 ** attempt);
|
|
80
78
|
await sleep(backoff / 2 + Math.random() * (backoff / 2));
|
|
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { atomicWriteFile, canonicalStoreKey } from "../file/fs-atomic.js";
|
|
5
5
|
import { withCcLock } from "./lockfile.js";
|
|
6
|
+
const INBOX_FILE_EXT = ".json";
|
|
6
7
|
export function sanitizeCcAgentName(name) {
|
|
7
8
|
return name.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
|
|
8
9
|
}
|
|
@@ -23,10 +24,8 @@ export function createCcFileMailboxStore(opts) {
|
|
|
23
24
|
throw new Error(`CcFileMailboxStore: CC inbox files carry no tenant axis — scope:"default" only (D-1); got ${JSON.stringify(scope)}`);
|
|
24
25
|
}
|
|
25
26
|
};
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return join(inboxDir, `${sanitizeCcAgentName(name)}.json`);
|
|
29
|
-
};
|
|
27
|
+
const leaseKey = (handle) => sanitizeCcAgentName(opts.agentNameOf?.(handle) ?? handle);
|
|
28
|
+
const inboxPath = (handle) => join(inboxDir, `${leaseKey(handle)}${INBOX_FILE_EXT}`);
|
|
30
29
|
const discloseCorrupt = (path, reason) => {
|
|
31
30
|
try {
|
|
32
31
|
opts.onCorruptRead?.({ path, reason });
|
|
@@ -98,6 +97,42 @@ export function createCcFileMailboxStore(opts) {
|
|
|
98
97
|
const saveBox = (path, box) => {
|
|
99
98
|
atomicWriteFile(inboxDir, path, JSON.stringify(box, null, 2));
|
|
100
99
|
};
|
|
100
|
+
const isColderThan = (box, now, maxAgeMs) => {
|
|
101
|
+
if (box.length === 0)
|
|
102
|
+
return false;
|
|
103
|
+
const newest = box.reduce((acc, e) => {
|
|
104
|
+
const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : Number.NaN;
|
|
105
|
+
return Number.isFinite(t) ? Math.max(acc, t) : Number.POSITIVE_INFINITY;
|
|
106
|
+
}, 0);
|
|
107
|
+
return newest !== Number.POSITIVE_INFINITY && now - newest > maxAgeMs;
|
|
108
|
+
};
|
|
109
|
+
const sweepColdBoxes = async (now, maxAgeMs) => {
|
|
110
|
+
let dropped = 0;
|
|
111
|
+
for (const f of readdirSync(inboxDir)) {
|
|
112
|
+
if (!f.endsWith(INBOX_FILE_EXT))
|
|
113
|
+
continue;
|
|
114
|
+
const path = join(inboxDir, f);
|
|
115
|
+
const boxKey = f.slice(0, -INBOX_FILE_EXT.length);
|
|
116
|
+
const preNotes = deferredNotes();
|
|
117
|
+
if (!isColderThan(loadBox(path, preNotes.note), now, maxAgeMs)) {
|
|
118
|
+
preNotes.flush();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const notes = deferredNotes();
|
|
122
|
+
const swept = await withCcLock(path, () => {
|
|
123
|
+
if (!isColderThan(loadBox(path, notes.note), now, maxAgeMs))
|
|
124
|
+
return false;
|
|
125
|
+
leases.delete(boxKey);
|
|
126
|
+
rmSync(path, { force: true });
|
|
127
|
+
return true;
|
|
128
|
+
}).finally(() => {
|
|
129
|
+
notes.flush();
|
|
130
|
+
});
|
|
131
|
+
if (swept)
|
|
132
|
+
dropped += 1;
|
|
133
|
+
}
|
|
134
|
+
return dropped;
|
|
135
|
+
};
|
|
101
136
|
return {
|
|
102
137
|
append: (scope, handle, msg) => {
|
|
103
138
|
requireDefaultScope(scope);
|
|
@@ -124,11 +159,12 @@ export function createCcFileMailboxStore(opts) {
|
|
|
124
159
|
claimLease: (scope, handle, owner, ttlMs, now) => {
|
|
125
160
|
requireDefaultScope(scope);
|
|
126
161
|
const t = now ?? opts.now?.() ?? Date.now();
|
|
127
|
-
const key =
|
|
162
|
+
const key = leaseKey(handle);
|
|
163
|
+
const path = inboxPath(handle);
|
|
128
164
|
const held = leases.get(key);
|
|
129
165
|
if (held !== undefined && held.expiresAt > t && held.owner !== owner)
|
|
130
166
|
return Promise.resolve(null);
|
|
131
|
-
const box = loadBox(
|
|
167
|
+
const box = loadBox(path, discloseCorrupt);
|
|
132
168
|
const maxSeq = box.length;
|
|
133
169
|
const pending = box
|
|
134
170
|
.map((e, i) => ({ e, seq: i + 1 }))
|
|
@@ -147,7 +183,7 @@ export function createCcFileMailboxStore(opts) {
|
|
|
147
183
|
ack: (scope, handle, owner, upToSeq) => {
|
|
148
184
|
requireDefaultScope(scope);
|
|
149
185
|
const path = inboxPath(handle);
|
|
150
|
-
const held = leases.get(
|
|
186
|
+
const held = leases.get(leaseKey(handle));
|
|
151
187
|
if (held === undefined || held.owner !== owner)
|
|
152
188
|
return Promise.resolve();
|
|
153
189
|
if (!existsSync(path))
|
|
@@ -165,14 +201,14 @@ export function createCcFileMailboxStore(opts) {
|
|
|
165
201
|
if (changed)
|
|
166
202
|
saveBox(path, box);
|
|
167
203
|
if (held.maxSeq <= upToSeq)
|
|
168
|
-
leases.delete(
|
|
204
|
+
leases.delete(leaseKey(handle));
|
|
169
205
|
}).finally(() => {
|
|
170
206
|
notes.flush();
|
|
171
207
|
});
|
|
172
208
|
},
|
|
173
209
|
releaseLease: (scope, handle, owner) => {
|
|
174
210
|
requireDefaultScope(scope);
|
|
175
|
-
const key =
|
|
211
|
+
const key = leaseKey(handle);
|
|
176
212
|
const held = leases.get(key);
|
|
177
213
|
if (held !== undefined && held.owner === owner)
|
|
178
214
|
leases.delete(key);
|
|
@@ -185,42 +221,17 @@ export function createCcFileMailboxStore(opts) {
|
|
|
185
221
|
drop: (scope, handle) => {
|
|
186
222
|
requireDefaultScope(scope);
|
|
187
223
|
const path = inboxPath(handle);
|
|
188
|
-
leases.delete(
|
|
189
|
-
|
|
224
|
+
leases.delete(leaseKey(handle));
|
|
225
|
+
return withCcLock(path, () => {
|
|
190
226
|
rmSync(path, { force: true });
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
}
|
|
195
|
-
return Promise.resolve();
|
|
227
|
+
});
|
|
196
228
|
},
|
|
197
229
|
reap: (scope, now, reapOpts) => {
|
|
198
230
|
requireDefaultScope(scope);
|
|
199
231
|
const maxAgeMs = reapOpts?.maxAgeMs;
|
|
200
232
|
if (maxAgeMs === undefined || !existsSync(inboxDir))
|
|
201
233
|
return Promise.resolve(0);
|
|
202
|
-
|
|
203
|
-
for (const f of readdirSync(inboxDir)) {
|
|
204
|
-
if (!f.endsWith(".json"))
|
|
205
|
-
continue;
|
|
206
|
-
const path = join(inboxDir, f);
|
|
207
|
-
const box = loadBox(path, discloseCorrupt);
|
|
208
|
-
const newest = box.reduce((acc, e) => {
|
|
209
|
-
const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : Number.NaN;
|
|
210
|
-
return Number.isFinite(t) ? Math.max(acc, t) : Number.POSITIVE_INFINITY;
|
|
211
|
-
}, 0);
|
|
212
|
-
if (box.length > 0 && newest !== Number.POSITIVE_INFINITY && now - newest > maxAgeMs) {
|
|
213
|
-
leases.delete(path);
|
|
214
|
-
try {
|
|
215
|
-
rmSync(path, { force: true });
|
|
216
|
-
rmSync(`${path}.lock`, { recursive: true, force: true });
|
|
217
|
-
dropped += 1;
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return Promise.resolve(dropped);
|
|
234
|
+
return sweepColdBoxes(now, maxAgeMs);
|
|
224
235
|
},
|
|
225
236
|
};
|
|
226
237
|
}
|
|
@@ -7,6 +7,7 @@ import { type FileCheckpointStoreOptions } from "./checkpoint-store.js";
|
|
|
7
7
|
import type { SessionPolicyStore } from "../../core/session-policy-store.js";
|
|
8
8
|
import type { FileSnapshotStore } from "../../core/file-snapshot-store.js";
|
|
9
9
|
import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
|
|
10
|
+
import type { UsageWindowStore } from "../../core/usage-window-store.js";
|
|
10
11
|
export { FileCheckpointStore, type FileCheckpointStoreOptions } from "./checkpoint-store.js";
|
|
11
12
|
export { FileMemoryStore } from "./memory-store.js";
|
|
12
13
|
export { FileSessionRepo, type FileSessionRepoOptions } from "./session-store.js";
|
|
@@ -14,6 +15,7 @@ export { FileToolResultStore } from "./tool-result-store.js";
|
|
|
14
15
|
export { FileSessionPolicyStore, type FileSessionPolicyStoreOptions, type SessionPolicyCorruptReadInfo } from "./session-policy-store.js";
|
|
15
16
|
export { FileFileSnapshotStore, type FileFileSnapshotStoreOptions } from "./file-snapshot-store.js";
|
|
16
17
|
export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
|
|
18
|
+
export { FileUsageWindowStore } from "./usage-window-store.js";
|
|
17
19
|
export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
|
|
18
20
|
export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
|
|
19
21
|
export interface FileStorageBackendOptions {
|
|
@@ -40,6 +42,7 @@ export declare class FileStorageBackend {
|
|
|
40
42
|
readonly sessionPolicyStore: SessionPolicyStore;
|
|
41
43
|
readonly fileSnapshotStore: FileSnapshotStore;
|
|
42
44
|
readonly workflowJournalStore: WorkflowJournalStore;
|
|
45
|
+
readonly usageWindowStore: UsageWindowStore;
|
|
43
46
|
readonly consolidationLock: (scope: string) => (() => void) | undefined;
|
|
44
47
|
private readonly lock;
|
|
45
48
|
private readonly fileCheckpoints;
|
|
@@ -9,6 +9,7 @@ import { FileToolResultStore } from "./tool-result-store.js";
|
|
|
9
9
|
import { FileSessionPolicyStore } from "./session-policy-store.js";
|
|
10
10
|
import { FileFileSnapshotStore } from "./file-snapshot-store.js";
|
|
11
11
|
import { FileWorkflowJournalStore } from "./workflow-journal-store.js";
|
|
12
|
+
import { FileUsageWindowStore } from "./usage-window-store.js";
|
|
12
13
|
export { FileCheckpointStore } from "./checkpoint-store.js";
|
|
13
14
|
export { FileMemoryStore } from "./memory-store.js";
|
|
14
15
|
export { FileSessionRepo } from "./session-store.js";
|
|
@@ -16,6 +17,7 @@ export { FileToolResultStore } from "./tool-result-store.js";
|
|
|
16
17
|
export { FileSessionPolicyStore } from "./session-policy-store.js";
|
|
17
18
|
export { FileFileSnapshotStore } from "./file-snapshot-store.js";
|
|
18
19
|
export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
|
|
20
|
+
export { FileUsageWindowStore } from "./usage-window-store.js";
|
|
19
21
|
export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
|
|
20
22
|
export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
|
|
21
23
|
export class FileStorageBackend {
|
|
@@ -27,6 +29,7 @@ export class FileStorageBackend {
|
|
|
27
29
|
sessionPolicyStore;
|
|
28
30
|
fileSnapshotStore;
|
|
29
31
|
workflowJournalStore;
|
|
32
|
+
usageWindowStore;
|
|
30
33
|
consolidationLock;
|
|
31
34
|
lock;
|
|
32
35
|
fileCheckpoints;
|
|
@@ -53,6 +56,7 @@ export class FileStorageBackend {
|
|
|
53
56
|
this.fileSnapshotStore = new FileFileSnapshotStore(this.root, opts.snapshotBounds, corruptRead);
|
|
54
57
|
this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root);
|
|
55
58
|
this.workflowJournalStore = this.fileWorkflowJournal;
|
|
59
|
+
this.usageWindowStore = new FileUsageWindowStore(this.root);
|
|
56
60
|
this.consolidationLock = createFileConsolidationLock(join(this.root, "consolidation-locks"));
|
|
57
61
|
}
|
|
58
62
|
catch (err) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type UsageWindow, type UsageWindowReading, type UsageWindowStore } from "../../core/usage-window-store.js";
|
|
2
|
+
export declare class FileUsageWindowStore implements UsageWindowStore {
|
|
3
|
+
private readonly dir;
|
|
4
|
+
constructor(root: string);
|
|
5
|
+
private pathFor;
|
|
6
|
+
private loadRecord;
|
|
7
|
+
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[]): Promise<void>;
|
|
8
|
+
read(key: string, windows: readonly UsageWindow[], now: number): Promise<readonly UsageWindowReading[]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { chargeUsageRecord, readUsageRecord, EMPTY_USAGE_WINDOW_RECORD, } from "../../core/usage-window-store.js";
|
|
4
|
+
import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
|
|
5
|
+
export class FileUsageWindowStore {
|
|
6
|
+
dir;
|
|
7
|
+
constructor(root) {
|
|
8
|
+
this.dir = join(root, "usage-windows");
|
|
9
|
+
ensureDir(this.dir);
|
|
10
|
+
}
|
|
11
|
+
pathFor(key) {
|
|
12
|
+
return join(this.dir, `${sanitizeScope(key)}.json`);
|
|
13
|
+
}
|
|
14
|
+
loadRecord(key) {
|
|
15
|
+
const path = this.pathFor(key);
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = readFileSync(path, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err.code === "ENOENT")
|
|
22
|
+
return EMPTY_USAGE_WINDOW_RECORD;
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw corrupt(path, "unparseable JSON (torn/zero-byte write?)");
|
|
31
|
+
}
|
|
32
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
33
|
+
throw corrupt(path, "not a JSON object");
|
|
34
|
+
const rec = parsed;
|
|
35
|
+
const slots = validateSlots(rec.slots, path);
|
|
36
|
+
const buckets = validateBuckets(rec.buckets, path);
|
|
37
|
+
return { slots, buckets };
|
|
38
|
+
}
|
|
39
|
+
async charge(key, tokens, at, windows) {
|
|
40
|
+
const next = chargeUsageRecord(this.loadRecord(key), tokens, at, windows);
|
|
41
|
+
atomicWriteFile(join(this.dir, "tmp"), this.pathFor(key), JSON.stringify(next));
|
|
42
|
+
}
|
|
43
|
+
async read(key, windows, now) {
|
|
44
|
+
return readUsageRecord(this.loadRecord(key), windows, now);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function corrupt(path, reason) {
|
|
48
|
+
const e = new Error(`usage-window ledger at ${path} is unreadable (${reason}). Refused rather than treated as empty: an unreadable ceiling would grant the key an unlimited, silent allowance.`);
|
|
49
|
+
e.code = "usage_window.store_corrupt";
|
|
50
|
+
return e;
|
|
51
|
+
}
|
|
52
|
+
function validateSlots(value, path) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
if (!Array.isArray(value))
|
|
56
|
+
throw corrupt(path, "`slots` is not an array");
|
|
57
|
+
return value.map((s) => {
|
|
58
|
+
if (s === null || typeof s !== "object")
|
|
59
|
+
throw corrupt(path, "`slots` entry is not an object");
|
|
60
|
+
const { at, tokens } = s;
|
|
61
|
+
if (typeof at !== "number" || !Number.isFinite(at) || typeof tokens !== "number" || !Number.isFinite(tokens)) {
|
|
62
|
+
throw corrupt(path, "`slots` entry has a non-numeric `at`/`tokens`");
|
|
63
|
+
}
|
|
64
|
+
return { at, tokens };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function validateBuckets(value, path) {
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
return [];
|
|
70
|
+
if (!Array.isArray(value))
|
|
71
|
+
throw corrupt(path, "`buckets` is not an array");
|
|
72
|
+
return value.map((b) => {
|
|
73
|
+
if (b === null || typeof b !== "object")
|
|
74
|
+
throw corrupt(path, "`buckets` entry is not an object");
|
|
75
|
+
const { windowMs, openedAt, tokens } = b;
|
|
76
|
+
if (typeof windowMs !== "number" ||
|
|
77
|
+
!Number.isFinite(windowMs) ||
|
|
78
|
+
typeof openedAt !== "number" ||
|
|
79
|
+
!Number.isFinite(openedAt) ||
|
|
80
|
+
typeof tokens !== "number" ||
|
|
81
|
+
!Number.isFinite(tokens)) {
|
|
82
|
+
throw corrupt(path, "`buckets` entry has a non-numeric `windowMs`/`openedAt`/`tokens`");
|
|
83
|
+
}
|
|
84
|
+
return { windowMs, openedAt, tokens };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -5,14 +5,6 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
|
5
5
|
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => {
|
|
6
6
|
reversible: boolean;
|
|
7
7
|
};
|
|
8
|
-
export interface ExecClampOption {
|
|
9
|
-
deadlineMs: () => number | undefined;
|
|
10
|
-
onClamp?: (requestedSec: number, clampedSec: number) => void;
|
|
11
|
-
registerKillHandle?: (toolCallId: string, handle: {
|
|
12
|
-
pid: number;
|
|
13
|
-
kill: () => void;
|
|
14
|
-
}) => () => void;
|
|
15
|
-
}
|
|
16
8
|
export declare function bashExitOneInterpretation(command: string): string | undefined;
|
|
17
9
|
export declare function canAutoBackground(command: string, depth?: number): boolean;
|
|
18
10
|
export declare function createBashTool(env: ExecutionEnv, rootCanonical: string, coAuthor?: string | false, cwdRef?: CwdRef, taskOpts?: {
|
|
@@ -24,14 +16,13 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
24
16
|
priority?: "now" | "next" | "later";
|
|
25
17
|
}) => void;
|
|
26
18
|
detachHub?: import("../../core/tool-detach.js").ToolDetachHub;
|
|
27
|
-
execClamp?: ExecClampOption;
|
|
28
19
|
autoBackgroundOnTimeout?: boolean;
|
|
29
20
|
oneShot?: boolean;
|
|
30
21
|
additionalRoots?: readonly string[];
|
|
31
22
|
bashDefaultTimeoutMs?: number;
|
|
32
23
|
bashMaxTimeoutMs?: number;
|
|
33
24
|
}): AgentTool;
|
|
34
|
-
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>,
|
|
25
|
+
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, opts?: {
|
|
35
26
|
bashDefaultTimeoutMs?: number;
|
|
36
27
|
bashMaxTimeoutMs?: number;
|
|
37
28
|
additionalRoots?: readonly string[];
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -231,61 +231,27 @@ function msTimeoutToRequestedSec(timeoutMs) {
|
|
|
231
231
|
return undefined;
|
|
232
232
|
return Math.max(1, Math.round(timeoutMs / 1000));
|
|
233
233
|
}
|
|
234
|
-
async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, cwdRef, detach,
|
|
234
|
+
async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, cwdRef, detach, readOnly, containmentRoots) {
|
|
235
235
|
const requestedSec = Math.max(1, Math.floor(timeoutSec ?? caps.defaultSec));
|
|
236
236
|
let timeout = Math.min(caps.maxSec, requestedSec);
|
|
237
237
|
const cappedByMaxTimeout = requestedSec > caps.maxSec;
|
|
238
|
-
let clampedByDeadline = false;
|
|
239
|
-
let deadlineExhausted = false;
|
|
240
|
-
if (execClamp) {
|
|
241
|
-
const deadline = execClamp.deadlineMs();
|
|
242
|
-
if (deadline !== undefined) {
|
|
243
|
-
const rawRemainSec = Math.floor((deadline - Date.now()) / 1000);
|
|
244
|
-
const maxSec = Math.max(1, rawRemainSec);
|
|
245
|
-
if (maxSec < timeout) {
|
|
246
|
-
execClamp.onClamp?.(requestedSec, maxSec);
|
|
247
|
-
timeout = maxSec;
|
|
248
|
-
clampedByDeadline = true;
|
|
249
|
-
deadlineExhausted = rawRemainSec < 1;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
238
|
const effectiveCwd = cwdRef ? cwdRef.current : cwd;
|
|
254
239
|
const toRun = cwdRef
|
|
255
240
|
? `${command}\n__cc_rc=$?; printf '%s%s%s' '${CWD_SENTINEL}' "$(pwd)" '${CWD_SENTINEL}' 1>&2; exit $__cc_rc`
|
|
256
241
|
: command;
|
|
257
242
|
const execEnv = cwdRef ? { env: { PWD: effectiveCwd } } : undefined;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
onDetachAdopted: () => {
|
|
271
|
-
deregisterKill?.();
|
|
272
|
-
deregisterKill = undefined;
|
|
273
|
-
},
|
|
274
|
-
autoBackgroundOnTimeout: detach.autoBackgroundEligible === true && !clampedByDeadline,
|
|
275
|
-
}
|
|
276
|
-
: {}),
|
|
277
|
-
...(registerKillHandle !== undefined && toolCallId !== undefined
|
|
278
|
-
? {
|
|
279
|
-
onSpawn: (handle) => {
|
|
280
|
-
deregisterKill = registerKillHandle(toolCallId, handle);
|
|
281
|
-
},
|
|
282
|
-
}
|
|
283
|
-
: {}),
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
deregisterKill?.();
|
|
288
|
-
}
|
|
243
|
+
const res = await env.exec(toRun, {
|
|
244
|
+
cwd: effectiveCwd,
|
|
245
|
+
timeout,
|
|
246
|
+
abortSignal: signal,
|
|
247
|
+
...execEnv,
|
|
248
|
+
...(detach
|
|
249
|
+
? {
|
|
250
|
+
detachSignal: detach.signal,
|
|
251
|
+
autoBackgroundOnTimeout: detach.autoBackgroundEligible === true,
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
254
|
+
});
|
|
289
255
|
if (!res.ok) {
|
|
290
256
|
if (res.error.code === "spawn_error" || res.error.code === "shell_unavailable") {
|
|
291
257
|
const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef);
|
|
@@ -321,13 +287,9 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
321
287
|
const pStdout = clipShellOutput(rawStdout);
|
|
322
288
|
const pStderr = clipShellOutput(rawStderr);
|
|
323
289
|
const headline = res.error.code === "timeout"
|
|
324
|
-
?
|
|
325
|
-
?
|
|
326
|
-
|
|
327
|
-
: `Command was cut off after ${timeout}s — NOT its requested ${requestedSec}s timeout: the task is near its wall-clock deadline, so the foreground time budget was clamped to the ${timeout}s remaining. The process was killed; output produced before the cutoff is shown below. Do NOT retry this foreground command — the next attempt gets even less time. Use run_in_background (exempt from this clamp) if the work must continue, or write out your results now.`
|
|
328
|
-
: cappedByMaxTimeout
|
|
329
|
-
? `Command timed out after ${timeout}s — requested ${requestedSec}s, capped at ${caps.maxSec}s (engine ceiling: requests above ${caps.maxSec}s are reduced to it). The process was killed; output produced before the cutoff is shown below. Re-running with a larger timeout gets the same ${caps.maxSec}s ceiling — use run_in_background for work that needs longer, a narrower command, or resume from the partial progress below.`
|
|
330
|
-
: `Command timed out after ${timeout}s (timeout limit: ${timeout}s). The process was killed; output produced before the cutoff is shown below. Re-running the same command will likely time out again — consider run_in_background for long commands, a narrower command, or resuming from the partial progress below.`
|
|
290
|
+
? cappedByMaxTimeout
|
|
291
|
+
? `Command timed out after ${timeout}s — requested ${requestedSec}s, capped at ${caps.maxSec}s (engine ceiling: requests above ${caps.maxSec}s are reduced to it). The process was killed; output produced before the cutoff is shown below. Re-running with a larger timeout gets the same ${caps.maxSec}s ceiling — use run_in_background for work that needs longer, a narrower command, or resume from the partial progress below.`
|
|
292
|
+
: `Command timed out after ${timeout}s (timeout limit: ${timeout}s). The process was killed; output produced before the cutoff is shown below. Re-running the same command will likely time out again — consider run_in_background for long commands, a narrower command, or resuming from the partial progress below.`
|
|
331
293
|
: res.error.code === "aborted"
|
|
332
294
|
? `Command was interrupted (aborted) before completion. Output produced before the interrupt is shown below.`
|
|
333
295
|
: `Command started but its completion could not be observed — a host-side output-stream callback failed mid-run and the process was terminated (${res.error.message}). Output captured before the cut is shown below; its effects up to that point may have landed. Verify before assuming it needs a full re-run.`;
|
|
@@ -350,9 +312,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
350
312
|
? {
|
|
351
313
|
timedOut: true,
|
|
352
314
|
timeoutSec: timeout,
|
|
353
|
-
...(
|
|
354
|
-
...(clampedByDeadline ? { clampedByDeadline: true } : {}),
|
|
355
|
-
...(cappedByMaxTimeout ? { cappedToMaxTimeoutSec: caps.maxSec } : {}),
|
|
315
|
+
...(cappedByMaxTimeout ? { requestedTimeoutSec: requestedSec, cappedToMaxTimeoutSec: caps.maxSec } : {}),
|
|
356
316
|
}
|
|
357
317
|
: res.error.code === "aborted"
|
|
358
318
|
? { aborted: true }
|
|
@@ -696,7 +656,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
696
656
|
: undefined;
|
|
697
657
|
let res;
|
|
698
658
|
try {
|
|
699
|
-
res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, cwdRef, detachChain,
|
|
659
|
+
res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, cwdRef, detachChain, false, [rootCanonical, ...(taskOpts.additionalRoots ?? [])]);
|
|
700
660
|
}
|
|
701
661
|
finally {
|
|
702
662
|
taskOpts.detachHub?.gc(ctx.toolCallId);
|
|
@@ -941,7 +901,7 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
941
901
|
},
|
|
942
902
|
};
|
|
943
903
|
}
|
|
944
|
-
export function createBashReadonlyTool(env, rootCanonical, allow,
|
|
904
|
+
export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
945
905
|
const timeoutCaps = resolveBashTimeoutCaps(opts);
|
|
946
906
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
947
907
|
const sample = [...allow].slice(0, 6).join(", ");
|
|
@@ -996,7 +956,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
|
|
|
996
956
|
return errorResult(`Error (Bash): a path this command reads resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")} — not auto-allowed. ` +
|
|
997
957
|
"bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.", { code: "readonly_out_of_root", paths: resolved.escaping });
|
|
998
958
|
}
|
|
999
|
-
return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined,
|
|
959
|
+
return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true);
|
|
1000
960
|
},
|
|
1001
961
|
});
|
|
1002
962
|
}
|