@sema-agent/core 5.7.0 → 5.9.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 +79 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/cascade.js +24 -0
- package/dist/agents/subagent.d.ts +7 -7
- package/dist/agents/subagent.js +74 -33
- 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/open-responses.d.ts +11 -0
- package/dist/brain/open-responses.js +721 -0
- package/dist/brain/openai.js +22 -33
- package/dist/brain/request-params.d.ts +1 -0
- package/dist/brain/request-params.js +16 -0
- 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/fs-write-gate-policy.js +2 -2
- package/dist/core/lsp-diagnostics.d.ts +3 -2
- package/dist/core/lsp-diagnostics.js +20 -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 +3 -8
- package/dist/core/runner/assemble-result.js +24 -18
- package/dist/core/runner/prepare-task.d.ts +27 -6
- package/dist/core/runner/prepare-task.js +209 -144
- package/dist/core/runner/runtask.d.ts +0 -2
- package/dist/core/runner/runtask.js +228 -266
- package/dist/core/runner/session-rule-policy.d.ts +1 -0
- package/dist/core/runner/session-rule-policy.js +4 -3
- 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-shared.d.ts +0 -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/tool-policy.d.ts +8 -0
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/trace.d.ts +1 -10
- package/dist/core/types.d.ts +27 -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 +1 -7
- package/dist/engine/harness/agent-harness.js +16 -53
- package/dist/engine/harness/types.d.ts +3 -15
- package/dist/engine/llm/types.d.ts +3 -75
- package/dist/engine/loop/agent-loop.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +7 -173
- package/dist/engine/loop/types.d.ts +1 -18
- package/dist/engine/session/repo-utils.d.ts +1 -2
- package/dist/engine/session/repo-utils.js +0 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +10 -5
- package/dist/internal/llm.d.ts +1 -1
- 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/orchestration/workflow.js +14 -5
- 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/dist/tools/web.js +20 -19
- package/package.json +4 -5
- 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
|
@@ -22,13 +22,7 @@ const WORKING_FILE_BREAKOUT_TAGS = ["working-file", "summary"];
|
|
|
22
22
|
function escapeWorkingFilePathAttr(path) {
|
|
23
23
|
return path.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/[\r\n]+/g, " ");
|
|
24
24
|
}
|
|
25
|
-
export const COMPACTION_THROUGHPUT_PRIOR_TOKENS_PER_MS = 3;
|
|
26
|
-
const COMPACTION_THROUGHPUT_EWMA_ALPHA = 0.3;
|
|
27
|
-
const COMPACTION_WALLTIME_ABORT_FLAG = "semaCompactionWalltimeAbort";
|
|
28
25
|
const COMPACTION_MANUAL_CANCEL_FLAG = "semaCompactionManualCancel";
|
|
29
|
-
export function isCompactionWalltimeAbort(err) {
|
|
30
|
-
return (typeof err === "object" && err !== null && err[COMPACTION_WALLTIME_ABORT_FLAG] === true);
|
|
31
|
-
}
|
|
32
26
|
export function isCompactionManualCancel(err) {
|
|
33
27
|
return (typeof err === "object" && err !== null && err[COMPACTION_MANUAL_CANCEL_FLAG] === true);
|
|
34
28
|
}
|
|
@@ -180,7 +174,7 @@ export async function maybeCompact(opts) {
|
|
|
180
174
|
const wantsWindowSafety = opts.compactionModel !== undefined &&
|
|
181
175
|
summarizationCharsCapacity(opts.compactionModel) < summarizationCharsCapacity(opts.model);
|
|
182
176
|
let dry;
|
|
183
|
-
if (wantsWindowSafety
|
|
177
|
+
if (wantsWindowSafety) {
|
|
184
178
|
dry = dryRunSummarizationClamp(prep.value, summaryModel, effectiveInstructions);
|
|
185
179
|
}
|
|
186
180
|
const contentTokens = dry !== undefined ? Math.ceil((dry.conversationChars + (prep.value.previousSummary?.length ?? 0)) / cpt) : 0;
|
|
@@ -201,13 +195,6 @@ export async function maybeCompact(opts) {
|
|
|
201
195
|
gate = "budget";
|
|
202
196
|
}
|
|
203
197
|
}
|
|
204
|
-
if (gate === undefined && opts.walltime !== undefined) {
|
|
205
|
-
const remainingMs = opts.walltime.deadlineMs - opts.walltime.cushionMs() - Date.now();
|
|
206
|
-
const throughput = opts.walltime.throughputRef.ewmaTokensPerMs ?? COMPACTION_THROUGHPUT_PRIOR_TOKENS_PER_MS;
|
|
207
|
-
if (remainingMs < contentTokens / Math.max(throughput, 1e-6)) {
|
|
208
|
-
gate = "walltime";
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
198
|
if (gate === undefined) {
|
|
212
199
|
summaryModel = opts.model;
|
|
213
200
|
modelFallback = true;
|
|
@@ -245,35 +232,7 @@ export async function maybeCompact(opts) {
|
|
|
245
232
|
}
|
|
246
233
|
}
|
|
247
234
|
const auth = await opts.getApiKeyAndHeaders?.(summaryModel);
|
|
248
|
-
|
|
249
|
-
let softTimer;
|
|
250
|
-
let softAbort;
|
|
251
|
-
let onOuterAbort;
|
|
252
|
-
let firstAbortCause;
|
|
253
|
-
const latchCallerCause = () => {
|
|
254
|
-
firstAbortCause ??= opts.manualCancelSignal?.aborted === true ? "manual" : "caller";
|
|
255
|
-
};
|
|
256
|
-
if (opts.walltime !== undefined) {
|
|
257
|
-
softAbort = new AbortController();
|
|
258
|
-
onOuterAbort = () => {
|
|
259
|
-
latchCallerCause();
|
|
260
|
-
softAbort?.abort();
|
|
261
|
-
};
|
|
262
|
-
if (opts.signal?.aborted) {
|
|
263
|
-
latchCallerCause();
|
|
264
|
-
softAbort.abort();
|
|
265
|
-
}
|
|
266
|
-
else if (opts.signal) {
|
|
267
|
-
opts.signal.addEventListener("abort", onOuterAbort, { once: true });
|
|
268
|
-
}
|
|
269
|
-
const softMs = opts.walltime.deadlineMs - opts.walltime.cushionMs() - Date.now();
|
|
270
|
-
softTimer = setTimeout(() => {
|
|
271
|
-
firstAbortCause ??= "walltime";
|
|
272
|
-
softAbort?.abort();
|
|
273
|
-
}, Math.max(0, softMs));
|
|
274
|
-
softTimer.unref?.();
|
|
275
|
-
summarySignal = softAbort.signal;
|
|
276
|
-
}
|
|
235
|
+
const summarySignal = opts.signal;
|
|
277
236
|
const runtime = brainToRuntime(opts.brain);
|
|
278
237
|
let res;
|
|
279
238
|
summaryStartAt = Date.now();
|
|
@@ -284,23 +243,9 @@ export async function maybeCompact(opts) {
|
|
|
284
243
|
}
|
|
285
244
|
finally {
|
|
286
245
|
summaryEndAt = Date.now();
|
|
287
|
-
if (softTimer !== undefined)
|
|
288
|
-
clearTimeout(softTimer);
|
|
289
|
-
if (onOuterAbort !== undefined)
|
|
290
|
-
opts.signal?.removeEventListener("abort", onOuterAbort);
|
|
291
246
|
}
|
|
292
247
|
if (!res.ok) {
|
|
293
|
-
|
|
294
|
-
softAbort.signal.aborted &&
|
|
295
|
-
(firstAbortCause !== undefined ? firstAbortCause === "walltime" : opts.signal?.aborted !== true);
|
|
296
|
-
if (walltimeWon) {
|
|
297
|
-
if (typeof res.error !== "object" || res.error === null) {
|
|
298
|
-
throw Object.assign(new Error(String(res.error)), { [COMPACTION_WALLTIME_ABORT_FLAG]: true });
|
|
299
|
-
}
|
|
300
|
-
res.error[COMPACTION_WALLTIME_ABORT_FLAG] = true;
|
|
301
|
-
}
|
|
302
|
-
if (!walltimeWon &&
|
|
303
|
-
opts.manualCancelSignal?.aborted === true &&
|
|
248
|
+
if (opts.manualCancelSignal?.aborted === true &&
|
|
304
249
|
res.error instanceof CompactionError &&
|
|
305
250
|
res.error.code === "aborted") {
|
|
306
251
|
res.error[COMPACTION_MANUAL_CANCEL_FLAG] = true;
|
|
@@ -315,15 +260,6 @@ export async function maybeCompact(opts) {
|
|
|
315
260
|
throw new Error(`compaction produced an oversized summary (≈${Math.ceil(summary.length / cpt)} tokens vs ` +
|
|
316
261
|
`${res.value.tokensBefore} before compaction; history left untouched)`);
|
|
317
262
|
}
|
|
318
|
-
if (opts.walltime !== undefined && contentTokens > 0) {
|
|
319
|
-
const passMs = Math.max(1, Date.now() - compactStartAt);
|
|
320
|
-
const sample = contentTokens / passMs;
|
|
321
|
-
const prev = opts.walltime.throughputRef.ewmaTokensPerMs;
|
|
322
|
-
opts.walltime.throughputRef.ewmaTokensPerMs =
|
|
323
|
-
prev === undefined
|
|
324
|
-
? sample
|
|
325
|
-
: COMPACTION_THROUGHPUT_EWMA_ALPHA * sample + (1 - COMPACTION_THROUGHPUT_EWMA_ALPHA) * prev;
|
|
326
|
-
}
|
|
327
263
|
firstKeptEntryId = res.value.firstKeptEntryId;
|
|
328
264
|
summaryTokensBefore = res.value.tokensBefore;
|
|
329
265
|
details = res.value.details;
|
|
@@ -10,7 +10,8 @@ export type CheckpointToken = string & {
|
|
|
10
10
|
readonly __brand: "CheckpointToken";
|
|
11
11
|
};
|
|
12
12
|
export declare function mintCheckpointToken(): CheckpointToken;
|
|
13
|
-
export type ResourceLimitReason = "
|
|
13
|
+
export type ResourceLimitReason = "budget_tokens" | "budget_cost" | "walltime" | "turns" | "preempt" | "env_lifetime" | "usage_window";
|
|
14
|
+
export type PlatformLimitReason = Extract<ResourceLimitReason, "env_lifetime" | "usage_window">;
|
|
14
15
|
export interface SafetyAxis {
|
|
15
16
|
egress?: boolean;
|
|
16
17
|
irreversible?: boolean;
|
|
@@ -53,6 +54,7 @@ export type CheckpointGate = {
|
|
|
53
54
|
} | {
|
|
54
55
|
kind: "resource_limit";
|
|
55
56
|
reason: ResourceLimitReason;
|
|
57
|
+
resumeAfterMs?: number;
|
|
56
58
|
} | {
|
|
57
59
|
kind: "needs_review";
|
|
58
60
|
reason: string;
|
|
@@ -153,12 +155,12 @@ export type SerializedCheckpointState = {
|
|
|
153
155
|
export declare const CURRENT_CHECKPOINT_VERSION = 1;
|
|
154
156
|
export declare const RESOURCE_CHECKPOINT_VERSION = 2;
|
|
155
157
|
export declare const BINDING_CHECKPOINT_VERSION = 3;
|
|
156
|
-
export declare const
|
|
157
|
-
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION =
|
|
158
|
+
export declare const TOKEN_CHECKPOINT_VERSION = 5;
|
|
159
|
+
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 5;
|
|
158
160
|
export declare function checkpointVersionOf(cp: Pick<Checkpoint, "version">): number;
|
|
159
161
|
export interface ResourceLedger {
|
|
160
162
|
totalBudgetMicroUsd?: number;
|
|
161
|
-
|
|
163
|
+
totalTokens?: number;
|
|
162
164
|
spentMicroUsd: number;
|
|
163
165
|
spentTokens: number;
|
|
164
166
|
spentTurns: number;
|
|
@@ -172,12 +174,12 @@ export declare function debitLedger(prior: ResourceLedger | undefined, slice: {
|
|
|
172
174
|
walltimeMs?: number;
|
|
173
175
|
}, total?: {
|
|
174
176
|
totalBudgetMicroUsd?: number;
|
|
175
|
-
|
|
177
|
+
totalTokens?: number;
|
|
176
178
|
}, opts?: {
|
|
177
179
|
countSlice?: boolean;
|
|
178
180
|
}): ResourceLedger;
|
|
179
181
|
export declare function remainingBudgetMicroUsd(ledger: ResourceLedger | undefined): number | undefined;
|
|
180
|
-
export declare function
|
|
182
|
+
export declare function remainingTokens(ledger: ResourceLedger | undefined): number | undefined;
|
|
181
183
|
export interface ResolvedOutcome {
|
|
182
184
|
boundCallId: string;
|
|
183
185
|
decision: "allow" | "deny" | "approve" | "reject" | "edit";
|
|
@@ -242,11 +244,11 @@ export interface CheckpointSummary {
|
|
|
242
244
|
}
|
|
243
245
|
export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
|
|
244
246
|
export declare class CheckpointError extends Error {
|
|
245
|
-
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch";
|
|
247
|
+
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.walltime_axis_retired" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch";
|
|
246
248
|
readonly detail?: {
|
|
247
249
|
field?: "boundCallId" | "boundInputHash" | "answer";
|
|
248
250
|
} | undefined;
|
|
249
|
-
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string, detail?: {
|
|
251
|
+
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.walltime_axis_retired" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string, detail?: {
|
|
250
252
|
field?: "boundCallId" | "boundInputHash" | "answer";
|
|
251
253
|
} | undefined);
|
|
252
254
|
}
|
|
@@ -126,8 +126,8 @@ export function buildRiskDescriptor(input) {
|
|
|
126
126
|
export const CURRENT_CHECKPOINT_VERSION = 1;
|
|
127
127
|
export const RESOURCE_CHECKPOINT_VERSION = 2;
|
|
128
128
|
export const BINDING_CHECKPOINT_VERSION = 3;
|
|
129
|
-
export const
|
|
130
|
-
export const MAX_SUPPORTED_CHECKPOINT_VERSION =
|
|
129
|
+
export const TOKEN_CHECKPOINT_VERSION = 5;
|
|
130
|
+
export const MAX_SUPPORTED_CHECKPOINT_VERSION = 5;
|
|
131
131
|
export function checkpointVersionOf(cp) {
|
|
132
132
|
return cp.version ?? 0;
|
|
133
133
|
}
|
|
@@ -140,7 +140,7 @@ function finiteSpend(v) {
|
|
|
140
140
|
export function debitLedger(prior, slice, total, opts) {
|
|
141
141
|
return {
|
|
142
142
|
totalBudgetMicroUsd: finiteBound(prior?.totalBudgetMicroUsd) ?? finiteBound(total?.totalBudgetMicroUsd),
|
|
143
|
-
|
|
143
|
+
totalTokens: finiteBound(prior?.totalTokens) ?? finiteBound(total?.totalTokens),
|
|
144
144
|
spentMicroUsd: (finiteSpend(prior?.spentMicroUsd) ?? 0) + Math.max(0, finiteSpend(slice.costMicroUsd) ?? 0),
|
|
145
145
|
spentTokens: (finiteSpend(prior?.spentTokens) ?? 0) + Math.max(0, finiteSpend(slice.tokens) ?? 0),
|
|
146
146
|
spentTurns: (finiteSpend(prior?.spentTurns) ?? 0) + Math.max(0, finiteSpend(slice.turns) ?? 0),
|
|
@@ -157,14 +157,14 @@ export function remainingBudgetMicroUsd(ledger) {
|
|
|
157
157
|
return 0;
|
|
158
158
|
return Math.max(0, total - (spent ?? 0));
|
|
159
159
|
}
|
|
160
|
-
export function
|
|
161
|
-
const total = ledger?.
|
|
160
|
+
export function remainingTokens(ledger) {
|
|
161
|
+
const total = ledger?.totalTokens;
|
|
162
162
|
if (total === undefined || !Number.isFinite(total))
|
|
163
163
|
return undefined;
|
|
164
|
-
const spent = ledger?.
|
|
164
|
+
const spent = ledger?.spentTokens;
|
|
165
165
|
if (spent !== undefined && !Number.isFinite(spent))
|
|
166
166
|
return 0;
|
|
167
|
-
return Math.max(0, total
|
|
167
|
+
return Math.max(0, total - (spent ?? 0));
|
|
168
168
|
}
|
|
169
169
|
export function summarizeCheckpoint(cp) {
|
|
170
170
|
const riskDescriptor = cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask" ? cp.gate.riskDescriptor : undefined;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../tools/fs/safety.js";
|
|
2
|
-
import {
|
|
2
|
+
import { PATH_CONFINABLE_WRITE_TOOLS, isWithin } from "./runner/session-rule-policy.js";
|
|
3
3
|
const ask = (message) => ({ action: "ask", message, decisionReason: "rule" });
|
|
4
4
|
export function createFsWriteGatePolicy(opts) {
|
|
5
5
|
const { env, rootPath, defaultWrite } = opts;
|
|
6
|
-
const gated =
|
|
6
|
+
const gated = PATH_CONFINABLE_WRITE_TOOLS;
|
|
7
7
|
const acceptDirs = opts.acceptDirs && opts.acceptDirs.length > 0 ? opts.acceptDirs : undefined;
|
|
8
8
|
const exemptDirs = opts.exemptDirs && opts.exemptDirs.length > 0 ? opts.exemptDirs : undefined;
|
|
9
9
|
return {
|
|
@@ -22,9 +22,10 @@ export declare class LspDiagnosticsRegistry {
|
|
|
22
22
|
private readonly pending;
|
|
23
23
|
private readonly delivered;
|
|
24
24
|
publish(uri: string, diagnostics: LspDiagnostic[]): void;
|
|
25
|
-
fileEdited(uri: string): void;
|
|
25
|
+
fileEdited(runIdent: string, uri: string): void;
|
|
26
|
+
releaseRun(runIdent: string): void;
|
|
26
27
|
isEmpty(): boolean;
|
|
27
|
-
drain(): LspFileDiagnostics[];
|
|
28
|
+
drain(runIdent: string): LspFileDiagnostics[];
|
|
28
29
|
}
|
|
29
30
|
export declare function formatDiagnosticsSummary(files: LspFileDiagnostics[]): string;
|
|
30
31
|
export declare function formatDiagnosticsBlock(files: LspFileDiagnostics[]): string;
|
|
@@ -16,30 +16,39 @@ function diagnosticKey(uri, d) {
|
|
|
16
16
|
}
|
|
17
17
|
export class LspDiagnosticsRegistry {
|
|
18
18
|
pending = new Map();
|
|
19
|
-
delivered = new
|
|
19
|
+
delivered = new Map();
|
|
20
20
|
publish(uri, diagnostics) {
|
|
21
21
|
if (diagnostics.length === 0)
|
|
22
22
|
this.pending.delete(uri);
|
|
23
23
|
else
|
|
24
24
|
this.pending.set(uri, diagnostics);
|
|
25
25
|
}
|
|
26
|
-
fileEdited(uri) {
|
|
26
|
+
fileEdited(runIdent, uri) {
|
|
27
|
+
const forRun = this.delivered.get(runIdent);
|
|
28
|
+
if (forRun === undefined)
|
|
29
|
+
return;
|
|
27
30
|
const prefix = `${uri}|`;
|
|
28
|
-
for (const key of
|
|
31
|
+
for (const key of forRun) {
|
|
29
32
|
if (key.startsWith(prefix))
|
|
30
|
-
|
|
33
|
+
forRun.delete(key);
|
|
31
34
|
}
|
|
35
|
+
if (forRun.size === 0)
|
|
36
|
+
this.delivered.delete(runIdent);
|
|
37
|
+
}
|
|
38
|
+
releaseRun(runIdent) {
|
|
39
|
+
this.delivered.delete(runIdent);
|
|
32
40
|
}
|
|
33
41
|
isEmpty() {
|
|
34
42
|
return this.pending.size === 0;
|
|
35
43
|
}
|
|
36
|
-
drain() {
|
|
44
|
+
drain(runIdent) {
|
|
37
45
|
const out = [];
|
|
38
46
|
const requeued = new Map();
|
|
47
|
+
let delivered = this.delivered.get(runIdent);
|
|
39
48
|
let total = 0;
|
|
40
49
|
for (const [uri, all] of this.pending) {
|
|
41
50
|
const fresh = all
|
|
42
|
-
.filter((d) =>
|
|
51
|
+
.filter((d) => delivered?.has(diagnosticKey(uri, d)) !== true)
|
|
43
52
|
.sort((a, b) => (a.severity ?? 99) - (b.severity ?? 99));
|
|
44
53
|
if (fresh.length === 0)
|
|
45
54
|
continue;
|
|
@@ -49,8 +58,12 @@ export class LspDiagnosticsRegistry {
|
|
|
49
58
|
requeued.set(uri, fresh.slice(take.length));
|
|
50
59
|
if (take.length === 0)
|
|
51
60
|
continue;
|
|
61
|
+
if (delivered === undefined) {
|
|
62
|
+
delivered = new Set();
|
|
63
|
+
this.delivered.set(runIdent, delivered);
|
|
64
|
+
}
|
|
52
65
|
for (const d of take)
|
|
53
|
-
|
|
66
|
+
delivered.add(diagnosticKey(uri, d));
|
|
54
67
|
total += take.length;
|
|
55
68
|
out.push({ uri, diagnostics: take });
|
|
56
69
|
}
|
|
@@ -487,28 +487,31 @@ export class FileMemoryEngineBackend {
|
|
|
487
487
|
mtimeMs = statSync(lockDir).mtimeMs;
|
|
488
488
|
}
|
|
489
489
|
catch {
|
|
490
|
-
|
|
490
|
+
mtimeMs = undefined;
|
|
491
491
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
statSync(ownerPath);
|
|
498
|
-
tokenPresent = true;
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
}
|
|
502
|
-
if (tokenPresent) {
|
|
503
|
-
const tomb = join(this.controlPlaneRoot, `txn.lock.stale-${randomUUID()}`);
|
|
492
|
+
if (mtimeMs !== undefined) {
|
|
493
|
+
const staleAt = mtimeMs + staleMs;
|
|
494
|
+
deadline = Math.max(deadline, staleAt + stealGraceMs);
|
|
495
|
+
if (Date.now() > staleAt) {
|
|
496
|
+
let tokenPresent = false;
|
|
504
497
|
try {
|
|
505
|
-
|
|
498
|
+
statSync(ownerPath);
|
|
499
|
+
tokenPresent = true;
|
|
506
500
|
}
|
|
507
501
|
catch {
|
|
508
|
-
continue;
|
|
509
502
|
}
|
|
510
|
-
|
|
511
|
-
|
|
503
|
+
if (tokenPresent) {
|
|
504
|
+
const tomb = join(this.controlPlaneRoot, `txn.lock.stale-${randomUUID()}`);
|
|
505
|
+
let stolen = false;
|
|
506
|
+
try {
|
|
507
|
+
renameSync(lockDir, tomb);
|
|
508
|
+
stolen = true;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
}
|
|
512
|
+
if (stolen)
|
|
513
|
+
rmSync(tomb, { recursive: true, force: true });
|
|
514
|
+
}
|
|
512
515
|
}
|
|
513
516
|
}
|
|
514
517
|
if (Date.now() > deadline) {
|
|
@@ -41,6 +41,11 @@ export declare const ANNOUNCEMENTS_FILE = "announcements.json";
|
|
|
41
41
|
export declare const MEMORY_ANNOUNCEMENTS_MAX = 20;
|
|
42
42
|
export declare const SCAN_FUSE_FILE = "scan-fuse.json";
|
|
43
43
|
export declare const SCAN_FUSE_THRESHOLD = 3;
|
|
44
|
+
export interface SidecarLockOptions {
|
|
45
|
+
onDeadline?: "proceed" | "throw";
|
|
46
|
+
waitCapMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export declare function acquireSidecarLock(lockDir: string, opts?: SidecarLockOptions): string;
|
|
44
49
|
interface AnnouncementsRecord {
|
|
45
50
|
folded: number;
|
|
46
51
|
queue: MemoryAnnouncement[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
@@ -36,6 +36,37 @@ function subtreeHasFiles(dir) {
|
|
|
36
36
|
}
|
|
37
37
|
return false;
|
|
38
38
|
}
|
|
39
|
+
function pruneFilelessDirTree(dir) {
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return !existsSync(dir);
|
|
46
|
+
}
|
|
47
|
+
for (const e of entries) {
|
|
48
|
+
if (!e.isDirectory())
|
|
49
|
+
return false;
|
|
50
|
+
if (!pruneFilelessDirTree(join(dir, e.name)))
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
rmdirSync(dir);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function tryAdoptByRename(fromDir, toDir) {
|
|
62
|
+
try {
|
|
63
|
+
renameSync(fromDir, toDir);
|
|
64
|
+
return { ok: true };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { ok: false, code: err.code, error: err };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
39
70
|
function migrateKeyedPartition(m) {
|
|
40
71
|
const disclose = (code, message) => {
|
|
41
72
|
try {
|
|
@@ -52,36 +83,26 @@ function migrateKeyedPartition(m) {
|
|
|
52
83
|
return;
|
|
53
84
|
if (!existsSync(m.fromDir))
|
|
54
85
|
return;
|
|
55
|
-
if (existsSync(m.toDir)) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
rmSync(m.toDir, { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
const code = err.code;
|
|
65
|
-
disclose("memory.partition_adopt_failed", `could not clear the empty directory scaffolding at ${m.toDir} before adopting ${m.fromDir} (${code ?? String(err)}): ` +
|
|
66
|
-
`this session proceeds on ${m.toDir}; ${m.fromDir} still holds the prior memory — move it by hand.`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
86
|
+
if (existsSync(m.toDir) && subtreeHasFiles(m.toDir)) {
|
|
87
|
+
disclose("memory.partition_split", splitText);
|
|
88
|
+
return;
|
|
69
89
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
ensureDirExists(dirname(m.toDir));
|
|
91
|
+
let outcome = tryAdoptByRename(m.fromDir, m.toDir);
|
|
92
|
+
if (!outcome.ok && (outcome.code === "EEXIST" || outcome.code === "ENOTEMPTY") && !subtreeHasFiles(m.toDir) && pruneFilelessDirTree(m.toDir)) {
|
|
93
|
+
outcome = tryAdoptByRename(m.fromDir, m.toDir);
|
|
94
|
+
}
|
|
95
|
+
if (outcome.ok)
|
|
96
|
+
return;
|
|
97
|
+
const { code, error } = outcome;
|
|
98
|
+
if ((code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") && existsSync(m.toDir)) {
|
|
99
|
+
if (!existsSync(m.fromDir))
|
|
80
100
|
return;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`${m.fromDir} still holds the prior memory — move it by hand.`);
|
|
101
|
+
disclose("memory.partition_split", splitText);
|
|
102
|
+
return;
|
|
84
103
|
}
|
|
104
|
+
disclose("memory.partition_adopt_failed", `could not adopt the memory partition ${m.fromDir} → ${m.toDir} (${code ?? String(error)}): this session proceeds on ${m.toDir}; ` +
|
|
105
|
+
`${m.fromDir} still holds the prior memory — move it by hand.`);
|
|
85
106
|
}
|
|
86
107
|
catch {
|
|
87
108
|
}
|
|
@@ -338,8 +359,13 @@ function sleepSync(ms) {
|
|
|
338
359
|
}
|
|
339
360
|
const LOCK_STALE_MS = 1_500;
|
|
340
361
|
const LOCK_WAIT_CAP_MS = 3_000;
|
|
341
|
-
|
|
342
|
-
|
|
362
|
+
const LOCK_SPIN_PAUSE_MS = 5;
|
|
363
|
+
const MAX_FAIL_OPEN_DEADLINE_ROUNDS = 3;
|
|
364
|
+
export function acquireSidecarLock(lockDir, opts = {}) {
|
|
365
|
+
const waitCapMs = opts.waitCapMs ?? LOCK_WAIT_CAP_MS;
|
|
366
|
+
const deadline = Date.now() + waitCapMs;
|
|
367
|
+
const mintToken = () => `${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
368
|
+
let deadlineRounds = 0;
|
|
343
369
|
const stealOnce = () => {
|
|
344
370
|
const tomb = `${lockDir}.stale-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
345
371
|
try {
|
|
@@ -354,7 +380,7 @@ function acquireSidecarLock(lockDir, opts = {}) {
|
|
|
354
380
|
for (;;) {
|
|
355
381
|
try {
|
|
356
382
|
mkdirSync(lockDir);
|
|
357
|
-
const token =
|
|
383
|
+
const token = mintToken();
|
|
358
384
|
let fd;
|
|
359
385
|
try {
|
|
360
386
|
fd = openSync(join(lockDir, "owner"), "w");
|
|
@@ -374,25 +400,24 @@ function acquireSidecarLock(lockDir, opts = {}) {
|
|
|
374
400
|
return token;
|
|
375
401
|
}
|
|
376
402
|
catch {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
if (Date.now() > deadline) {
|
|
388
|
-
if (opts.onDeadline === "throw") {
|
|
389
|
-
throw new Error(`sidecar lock wait timed out (${LOCK_WAIT_CAP_MS}ms) at ${lockDir} — refusing to proceed without the lock (fail-closed sidecar)`);
|
|
390
|
-
}
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const age = Date.now() - statSync(lockDir).mtimeMs;
|
|
406
|
+
if (age > LOCK_STALE_MS)
|
|
391
407
|
stealOnce();
|
|
392
|
-
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
}
|
|
411
|
+
if (Date.now() > deadline) {
|
|
412
|
+
if (opts.onDeadline === "throw") {
|
|
413
|
+
throw new Error(`sidecar lock wait timed out (${waitCapMs}ms) at ${lockDir} — refusing to proceed without the lock (fail-closed sidecar)`);
|
|
393
414
|
}
|
|
394
|
-
|
|
415
|
+
deadlineRounds += 1;
|
|
416
|
+
if (deadlineRounds > MAX_FAIL_OPEN_DEADLINE_ROUNDS)
|
|
417
|
+
return mintToken();
|
|
418
|
+
stealOnce();
|
|
395
419
|
}
|
|
420
|
+
sleepSync(LOCK_SPIN_PAUSE_MS);
|
|
396
421
|
}
|
|
397
422
|
}
|
|
398
423
|
function rollForwardSidecar(file, journal) {
|
|
@@ -30,14 +30,6 @@ export interface Stats {
|
|
|
30
30
|
}>;
|
|
31
31
|
};
|
|
32
32
|
mechanisms?: {
|
|
33
|
-
finalizeInjected?: true;
|
|
34
|
-
nudgesSent?: number;
|
|
35
|
-
capShrinks?: number;
|
|
36
|
-
callCutoffs?: number;
|
|
37
|
-
toolClamps?: number;
|
|
38
|
-
toolCuts?: number;
|
|
39
|
-
toolCutKills?: number;
|
|
40
|
-
pendingCutTools?: number;
|
|
41
33
|
finalVerifyInjected?: true;
|
|
42
34
|
finalVerifyInjections?: number;
|
|
43
35
|
attachmentsInjected?: number;
|
|
@@ -60,16 +52,19 @@ export interface Stats {
|
|
|
60
52
|
compactionMicroUsd: number;
|
|
61
53
|
};
|
|
62
54
|
}
|
|
55
|
+
export type BudgetAxis = "tokens" | "cost";
|
|
63
56
|
export interface ResultFlags {
|
|
64
57
|
threw: unknown;
|
|
65
58
|
model?: string;
|
|
66
59
|
unpricedSpend?: boolean;
|
|
67
60
|
rewindNotes?: TaskResult["rewindNotes"];
|
|
68
61
|
remoteEnvFailures?: TaskResult["remoteEnvFailures"];
|
|
62
|
+
retryAfterMs?: number;
|
|
69
63
|
abortedForTimeout: boolean;
|
|
70
64
|
abortedForTurns: boolean;
|
|
71
65
|
abortedLive?: boolean;
|
|
72
66
|
budgetHit?: "exceeded" | "precall";
|
|
67
|
+
budgetAxis?: BudgetAxis;
|
|
73
68
|
blockedReason?: string;
|
|
74
69
|
conflict?: boolean;
|
|
75
70
|
outputInvalid?: boolean;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { isDegenerateCutMessage
|
|
1
|
+
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
2
2
|
import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
|
|
3
|
+
const SALVAGE_ELIGIBLE_TERMINALS = new Set([
|
|
4
|
+
"output.degenerate",
|
|
5
|
+
"limits.max_tokens_exceeded",
|
|
6
|
+
"limits.max_cost_exceeded",
|
|
7
|
+
"limits.max_turns_exceeded",
|
|
8
|
+
"limits.max_walltime_exceeded",
|
|
9
|
+
"env.lifetime_expired",
|
|
10
|
+
"usage.window_exhausted",
|
|
11
|
+
]);
|
|
3
12
|
export function errorCodeOf(err) {
|
|
4
13
|
let cur = err;
|
|
5
14
|
let fallback;
|
|
@@ -54,7 +63,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
54
63
|
let checkpointGate;
|
|
55
64
|
let workspaceRestoreMode;
|
|
56
65
|
const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
|
|
57
|
-
const isWalltimeCutoff = final?.stopReason === "error" && isWalltimeCutMessage(final);
|
|
58
66
|
if (flags.outputInvalid) {
|
|
59
67
|
status = "failed";
|
|
60
68
|
errorCode = "output.invalid";
|
|
@@ -64,21 +72,15 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
64
72
|
status = "failed";
|
|
65
73
|
errorCode = "output.degenerate";
|
|
66
74
|
errorMessage = final?.errorMessage;
|
|
67
|
-
salvagedOutput = text.trim() || undefined;
|
|
68
|
-
}
|
|
69
|
-
else if (isWalltimeCutoff) {
|
|
70
|
-
status = "timeout";
|
|
71
|
-
errorCode = "limit.timeout";
|
|
72
|
-
errorMessage = final?.errorMessage;
|
|
73
|
-
salvagedOutput = text.trim() || undefined;
|
|
74
75
|
}
|
|
75
76
|
else if (flags.budgetHit) {
|
|
76
77
|
status = "failed";
|
|
77
|
-
errorCode = flags.
|
|
78
|
+
errorCode = flags.budgetAxis === "tokens" ? "limits.max_tokens_exceeded" : "limits.max_cost_exceeded";
|
|
79
|
+
const axisName = flags.budgetAxis === "tokens" ? "limits.maxTokens" : "limits.maxCostUsd";
|
|
78
80
|
errorMessage =
|
|
79
81
|
flags.budgetHit === "precall"
|
|
80
|
-
?
|
|
81
|
-
:
|
|
82
|
+
? `the estimated cost of the first call already exceeds ${axisName}; the task was not started`
|
|
83
|
+
: `cumulative usage exceeded ${axisName}`;
|
|
82
84
|
}
|
|
83
85
|
else if (flags.suspendLoop) {
|
|
84
86
|
status = "failed";
|
|
@@ -86,12 +88,12 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
86
88
|
errorMessage = "task suspended too many times (resume/restart loop) — exceeded the suspend limit";
|
|
87
89
|
}
|
|
88
90
|
else if (flags.threw) {
|
|
89
|
-
status =
|
|
91
|
+
status = "failed";
|
|
90
92
|
errorMessage = flags.threw instanceof Error ? flags.threw.message : String(flags.threw);
|
|
91
93
|
errorCode = flags.abortedForTimeout
|
|
92
|
-
? "
|
|
94
|
+
? "limits.max_walltime_exceeded"
|
|
93
95
|
: flags.abortedForTurns
|
|
94
|
-
? "
|
|
96
|
+
? "limits.max_turns_exceeded"
|
|
95
97
|
: flags.conflict
|
|
96
98
|
? "conflict"
|
|
97
99
|
: errorCodeOf(flags.threw);
|
|
@@ -114,8 +116,8 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
114
116
|
workspaceRestoreMode = flags.reviewRef.restoreMode;
|
|
115
117
|
}
|
|
116
118
|
else if (flags.abortedLive || final?.stopReason === "aborted") {
|
|
117
|
-
status =
|
|
118
|
-
errorCode = flags.abortedForTimeout ? "
|
|
119
|
+
status = "failed";
|
|
120
|
+
errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
|
|
119
121
|
errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
|
|
120
122
|
}
|
|
121
123
|
else if (!final) {
|
|
@@ -140,9 +142,13 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
140
142
|
else {
|
|
141
143
|
status = "completed";
|
|
142
144
|
}
|
|
145
|
+
if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
|
|
146
|
+
salvagedOutput = text.trim() || undefined;
|
|
147
|
+
}
|
|
148
|
+
const retryAfterMs = errorCode === "usage.window_exhausted" ? flags.retryAfterMs : undefined;
|
|
143
149
|
const { compactionMicroUsd: _internalCompaction, ...publicStats } = stats;
|
|
144
150
|
void _internalCompaction;
|
|
145
151
|
if (flags.unpricedSpend)
|
|
146
152
|
delete publicStats.costMicroUsd;
|
|
147
|
-
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), stats: publicStats };
|
|
153
|
+
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), stats: publicStats };
|
|
148
154
|
}
|