@desplega.ai/agent-swarm 1.156.0 → 1.158.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/dist/{acp-adapter-c1msptrw.js → acp-adapter-xaq4n6pc.js} +1 -1
- package/dist/{actions-wd41qtdc.js → actions-p9hz5yat.js} +15 -15
- package/dist/{app-tz2qrnah.js → app-g26wdcz6.js} +4 -4
- package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
- package/dist/{assistant-c3q1zj2t.js → assistant-wydfqkb3.js} +16 -16
- package/dist/{boot-reembed-0be1krkm.js → boot-reembed-g5ckg9yb.js} +6 -6
- package/dist/{boot-reembed-r00j03f3.js → boot-reembed-se8zmdcp.js} +5 -5
- package/dist/{boot-scrub-logs-9j4hd0vw.js → boot-scrub-logs-wfxqan61.js} +3 -3
- package/dist/{seed-pricing-ncgxz1r2.js → boot-seeds-s3q01xj3.js} +10 -10
- package/dist/{claude-adapter-112w28qc.js → claude-adapter-tv2x4f36.js} +7 -7
- package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
- package/dist/{claude-sdk-session-k7pb5kfz.js → claude-sdk-session-t442qz6f.js} +8 -8
- package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
- package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
- package/dist/{cli-mx3bzbv8.js → cli-20cj37vt.js} +1968 -44
- package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
- package/dist/{cli-24w39x6n.js → cli-47m6dkd3.js} +362 -126
- package/dist/{cli-90x9ewbz.js → cli-53sz8gkz.js} +1 -1
- package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
- package/dist/{cli-08n8jxcw.js → cli-65hjmev5.js} +57 -17
- package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
- package/dist/{cli-7j2098n5.js → cli-7gtvvpbh.js} +4 -4
- package/dist/{cli-b05d7szr.js → cli-7s3syk22.js} +18 -11
- package/dist/{cli-9w4ex8vh.js → cli-8qmme6vd.js} +2 -2
- package/dist/{cli-28wkjpdp.js → cli-987hg0aa.js} +2 -2
- package/dist/{cli-7pgcpcrg.js → cli-9httdpyq.js} +1 -1
- package/dist/{cli-jk26c1sz.js → cli-aky8tctm.js} +26 -93
- package/dist/{cli-865swy5p.js → cli-c07r9a2m.js} +3 -2
- package/dist/{cli-nyqza443.js → cli-fgqxwrpv.js} +291 -12
- package/dist/{cli-9pyah9c4.js → cli-ftfvggzx.js} +633 -166
- package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
- package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
- package/dist/{cli-y8bfxjtw.js → cli-j55n64b6.js} +23 -22
- package/dist/{cli-7vj52450.js → cli-j57qkrpq.js} +86 -28
- package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
- package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
- package/dist/{cli-tp0d7vzb.js → cli-rjehvrts.js} +4 -4
- package/dist/{cli-s8hpxeg4.js → cli-rqz8qbw7.js} +245 -75
- package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
- package/dist/cli-t1ze64h3.js +811 -0
- package/dist/{cli-hc2fkhdv.js → cli-ws6rqycm.js} +3 -2
- package/dist/{cli-t1cd3mhp.js → cli-wscdvcqz.js} +13 -5
- package/dist/{cli-xa4j64zh.js → cli-x24pshtq.js} +5 -5
- package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
- package/dist/{cli-szcyv1mm.js → cli-xhaeg2nc.js} +1 -1
- package/dist/cli.js +30 -16
- package/dist/{codex-adapter-g4tj1f2g.js → codex-adapter-cnm8wk11.js} +5 -5
- package/dist/{codex-session-runner-ad8ha7zt.js → codex-session-runner-gfg547zj.js} +5 -5
- package/dist/{commands-z3gd8ppc.js → commands-e3bn0p75.js} +3 -3
- package/dist/{db-bezm97r9.js → db-03v9kzr2.js} +17 -3
- package/dist/{dispatcher-a97hc33y.js → dispatcher-nrmg8yht.js} +14 -14
- package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
- package/dist/{handlers-wtdy1x02.js → handlers-aqcedbyb.js} +17 -17
- package/dist/{hook-7edgmzng.js → hook-7vkskkre.js} +19 -11
- package/dist/{hook-fvs58a5b.js → hook-ybvmrmhq.js} +4 -4
- package/dist/{http-phnf93hp.js → http-2eqa8t24.js} +432 -195
- package/dist/{index-7x131mmf.js → index-1yks3qcr.js} +14 -14
- package/dist/{index-1fktvwxc.js → index-a3vn96n8.js} +31 -28
- package/dist/{index-yp8rasxk.js → index-c6zsv1bd.js} +5 -5
- package/dist/{index-50jxgw0d.js → index-chgdkq36.js} +14 -14
- package/dist/{index-7jwk7nsm.js → index-x71bvq0p.js} +15 -15
- package/dist/{keepalive-nnz07ebd.js → keepalive-vtkf5n99.js} +5 -5
- package/dist/{lead-mg7qcgww.js → lead-h3sp357x.js} +26 -26
- package/dist/{maintenance-ywmnqrpx.js → maintenance-jzeqtk3q.js} +6 -6
- package/dist/{oauth-refresh-sweep-c63sqfs1.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
- package/dist/{onboard-2zj8sqkq.js → onboard-tmsp0qbz.js} +278 -81
- package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
- package/dist/{otel-impl-jgdfm26f.js → otel-impl-9mz73q65.js} +416 -301
- package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
- package/dist/{pricing-refresh-3whntb69.js → pricing-refresh-v1cbr9te.js} +5 -5
- package/dist/{rbac-roles-3zptcc3p.js → rbac-roles-c9wqz83x.js} +4 -4
- package/dist/{render-v2-dbvpyysc.js → render-v2-gryrh9h8.js} +4 -4
- package/dist/{setup-qwx0j8z9.js → setup-q9720bt6.js} +2 -2
- package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
- package/dist/{work-objects-sjbcmvep.js → work-objects-b76x7deb.js} +17 -17
- package/dist/{worker-xezqdkb7.js → worker-6vrxbv41.js} +26 -26
- package/openapi.json +1626 -629
- package/package.json +3 -2
- package/src/apps/catalog.generated.json +229 -0
- package/src/apps/page-validator.ts +71 -16
- package/src/apps/row-store.ts +66 -11
- package/src/apps/sync.ts +59 -17
- package/src/artifact-sdk/browser-sdk.ts +436 -2
- package/src/be/approval-sweeps.ts +109 -0
- package/src/be/boot-seeds.ts +40 -0
- package/src/be/chunking.ts +1 -1
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/approvals.ts +78 -0
- package/src/be/db/tasks/read.ts +197 -39
- package/src/be/db/tasks/write.ts +41 -9
- package/src/be/db-client.ts +107 -48
- package/src/be/db.ts +219 -76
- package/src/be/events.ts +33 -1
- package/src/be/memory/graph-expansion.ts +10 -3
- package/src/be/memory/providers/sqlite-store.ts +55 -13
- package/src/be/memory/raters/retrieval.ts +5 -2
- package/src/be/memory/reranker.ts +26 -2
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
- package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
- package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
- package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +1857 -0
- package/src/be/modelsdev-cache.ts +14 -1
- package/src/be/pricing-normalize.ts +2 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/steering.ts +53 -3
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/task-citations.ts +29 -0
- package/src/cli.tsx +14 -1
- package/src/commands/onboard/compose-generator.ts +8 -3
- package/src/commands/onboard/container-engine.ts +220 -0
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/health-check.tsx +4 -1
- package/src/commands/onboard/steps/prereq-check.tsx +50 -54
- package/src/commands/onboard/steps/start.tsx +31 -16
- package/src/commands/onboard/types.ts +7 -2
- package/src/commands/onboard.tsx +39 -4
- package/src/commands/profile-sync.ts +79 -26
- package/src/commands/runner.ts +8 -1
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +263 -103
- package/src/hooks/hook.ts +21 -12
- package/src/http/agents.ts +35 -0
- package/src/http/approval-requests.ts +136 -64
- package/src/http/apps.ts +14 -3
- package/src/http/config.ts +15 -1
- package/src/http/events.ts +62 -2
- package/src/http/index.ts +6 -4
- package/src/http/memory.ts +21 -7
- package/src/http/schedules.ts +1 -1
- package/src/http/session-data.ts +9 -4
- package/src/http/sessions.ts +25 -18
- package/src/http/stats.ts +19 -7
- package/src/http/status.ts +11 -1
- package/src/http/tasks.ts +40 -8
- package/src/http/webhooks.ts +67 -46
- package/src/http/workflows.ts +15 -20
- package/src/linear/outbound.ts +37 -15
- package/src/otel-impl.ts +151 -1
- package/src/otel.ts +14 -3
- package/src/prompts/memories.ts +19 -2
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/codex-agents-md.ts +140 -17
- package/src/providers/modelsdev-reasoning.json +551 -0
- package/src/rbac/legacy-policy.ts +16 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/deferred-task-waits.ts +28 -0
- package/src/scheduler/schedule-task.ts +3 -0
- package/src/scheduler/scheduler.ts +14 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +12 -7
- package/src/slack/app.ts +35 -4
- package/src/slack/connection-state.ts +17 -0
- package/src/tasks/context-key.ts +1 -0
- package/src/tasks/worker-follow-up.ts +6 -2
- package/src/tests/agent-task-activity.test.ts +74 -0
- package/src/tests/agents-list-model-display.test.ts +6 -1
- package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
- package/src/tests/approval-requests.test.ts +395 -0
- package/src/tests/approval-sweeps-config.test.ts +47 -0
- package/src/tests/approval-sweeps.test.ts +508 -0
- package/src/tests/apps-kibo-blocks.test.ts +77 -0
- package/src/tests/apps-spike.test.ts +56 -1
- package/src/tests/apps-sync-engine.test.ts +20 -9
- package/src/tests/boot-seeds.test.ts +170 -0
- package/src/tests/cancel-approval-request-tool.test.ts +115 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-adapter.test.ts +126 -1
- package/src/tests/config-key-filter.test.ts +80 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/db-client-busy-retry.test.ts +96 -0
- package/src/tests/db-client.test.ts +60 -1
- package/src/tests/db-task-read-contract.test.ts +10 -0
- package/src/tests/db-task-write-contract.test.ts +9 -0
- package/src/tests/deferred-schedule-reaper.test.ts +193 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/events-http.test.ts +103 -0
- package/src/tests/extensions-catalog.test.ts +22 -17
- package/src/tests/extensions-pre-task.test.ts +107 -0
- package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
- package/src/tests/follow-up-config-inheritance.test.ts +260 -0
- package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
- package/src/tests/heartbeat-reboot-live.test.ts +95 -0
- package/src/tests/heartbeat-stall-cas.test.ts +80 -0
- package/src/tests/heartbeat.test.ts +99 -3
- package/src/tests/hook-mcp-config.test.ts +29 -0
- package/src/tests/http-api-integration.test.ts +16 -2
- package/src/tests/kv-storage.test.ts +62 -0
- package/src/tests/linear-outbound-sync.test.ts +120 -5
- package/src/tests/list-endpoint-slimming.test.ts +91 -0
- package/src/tests/memory-graph-organic-route.test.ts +155 -0
- package/src/tests/memory-http-recall-gating.test.ts +14 -4
- package/src/tests/memory-hybrid.test.ts +161 -1
- package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
- package/src/tests/memory.test.ts +22 -0
- package/src/tests/onboard-container-engine.test.ts +240 -0
- package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
- package/src/tests/pricing-routes.test.ts +14 -0
- package/src/tests/profile-sync-warning.test.ts +8 -4
- package/src/tests/promote-draft-task-route.test.ts +19 -0
- package/src/tests/provider-registration.test.ts +296 -42
- package/src/tests/rbac-engine.test.ts +30 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/script-connections.test.ts +7 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions-detail-batch.test.ts +250 -0
- package/src/tests/sessions.test.ts +118 -0
- package/src/tests/slack-config.test.ts +22 -0
- package/src/tests/sonnet-5-5-registry.test.ts +44 -0
- package/src/tests/status.test.ts +36 -1
- package/src/tests/swarm-diff.test.ts +316 -10
- package/src/tests/task-completion-idempotency.test.ts +2 -1
- package/src/tests/tasks-requested-by-filter.test.ts +3 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/ui-list-slim-projections.test.ts +158 -0
- package/src/tests/webhooks-task-blocked.test.ts +127 -0
- package/src/tests/workflow-approval-recovery.test.ts +203 -0
- package/src/tests/workflow-hitl-routing.test.ts +22 -1
- package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
- package/src/tests/workflow-http-v2.test.ts +99 -8
- package/src/tests/workflow-retry-validation.test.ts +56 -0
- package/src/tests/workflow-tla-races.test.ts +389 -0
- package/src/tests/workflow-validation-port-routing.test.ts +113 -1
- package/src/tools/cancel-approval-request.ts +57 -0
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/request-human-input.ts +4 -2
- package/src/tools/schedules/list-schedules.ts +4 -2
- package/src/tools/send-task.ts +4 -0
- package/src/tools/templates.ts +19 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/workflows/list-workflow-runs.ts +1 -0
- package/src/types.ts +38 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/approval-cancel.ts +152 -0
- package/src/workflows/approval-notifications.ts +84 -0
- package/src/workflows/checkpoint.ts +10 -5
- package/src/workflows/definition.ts +21 -0
- package/src/workflows/engine.ts +152 -21
- package/src/workflows/executors/human-in-the-loop.ts +1 -1
- package/src/workflows/recovery.ts +100 -45
- package/src/workflows/resume.ts +49 -18
- package/src/workflows/retry-poller.ts +71 -21
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/github-sender-allowlist/README.md +18 -0
- package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
- package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
- package/templates/skills/apps/content.md +20 -1
- package/templates/skills/code-quality/content.md +26 -1
- package/templates/skills/pages/content.md +16 -4
- package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
- package/templates/skills/scheduling/content.md +1 -1
- package/dist/cli-2daksqps.js +0 -378
- package/dist/rbac-roles-3zw68x72.js +0 -37
package/src/otel-impl.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
2
|
+
import { format } from "node:util";
|
|
1
3
|
import {
|
|
4
|
+
type BatchObservableResult,
|
|
2
5
|
type Counter,
|
|
3
6
|
context,
|
|
4
7
|
type Gauge,
|
|
5
8
|
type Histogram,
|
|
9
|
+
type Meter,
|
|
6
10
|
metrics,
|
|
7
11
|
propagation,
|
|
8
12
|
ROOT_CONTEXT,
|
|
@@ -12,6 +16,7 @@ import {
|
|
|
12
16
|
type Tracer,
|
|
13
17
|
trace,
|
|
14
18
|
} from "@opentelemetry/api";
|
|
19
|
+
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
|
|
15
20
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
16
21
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
17
22
|
import {
|
|
@@ -149,7 +154,12 @@ export function resolveServiceName(serviceRole: string): string {
|
|
|
149
154
|
return serviceRole === "api" ? `${baseServiceName}-api` : baseServiceName;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
export
|
|
157
|
+
export interface BootOptions {
|
|
158
|
+
/** Mirror console output into OTel log records (API role only). */
|
|
159
|
+
exportConsoleLogs?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function boot(serviceRole: string, options: BootOptions = {}): Promise<void> {
|
|
153
163
|
if (sdk) return;
|
|
154
164
|
|
|
155
165
|
const configuredResourceAttributes = parseResourceAttributes();
|
|
@@ -191,6 +201,14 @@ export async function boot(serviceRole: string): Promise<void> {
|
|
|
191
201
|
|
|
192
202
|
sdk.start();
|
|
193
203
|
|
|
204
|
+
if (serviceRole === "api") {
|
|
205
|
+
startEventLoopDelayMetrics(metrics.getMeter(METER_NAME));
|
|
206
|
+
// NodeSDK already registers a global LoggerProvider exporting over OTLP
|
|
207
|
+
// to the same endpoint and resource as traces (OTEL_LOGS_EXPORTER
|
|
208
|
+
// defaults to otlp); nothing feeds it until the bridge is installed.
|
|
209
|
+
if (options.exportConsoleLogs) installConsoleLogBridge();
|
|
210
|
+
}
|
|
211
|
+
|
|
194
212
|
const shutdown = async () => {
|
|
195
213
|
try {
|
|
196
214
|
await sdk?.shutdown();
|
|
@@ -204,8 +222,140 @@ export async function boot(serviceRole: string): Promise<void> {
|
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
export async function shutdown(): Promise<void> {
|
|
225
|
+
// SDK first: its final flush still reads the event-loop gauges and drains
|
|
226
|
+
// bridged log records.
|
|
207
227
|
await sdk?.shutdown();
|
|
208
228
|
sdk = undefined;
|
|
229
|
+
stopEventLoopDelayMetrics();
|
|
230
|
+
uninstallConsoleLogBridge();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let stopEventLoopDelay: (() => void) | undefined;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Event-loop delay from `perf_hooks.monitorEventLoopDelay`, exported as the
|
|
237
|
+
* semconv `nodejs.eventloop.delay.*` gauges (seconds). Each collection reads
|
|
238
|
+
* the histogram and resets it, so `max` is the worst stall since the last
|
|
239
|
+
* export. Server spans only start once the loop frees, so a stall never shows
|
|
240
|
+
* in span durations: a request queued behind a 3s synchronous query still
|
|
241
|
+
* reports a 14ms span. This metric is what sees the stall.
|
|
242
|
+
*/
|
|
243
|
+
export function startEventLoopDelayMetrics(meter: Meter): void {
|
|
244
|
+
if (stopEventLoopDelay) return;
|
|
245
|
+
const histogram = monitorEventLoopDelay({ resolution: 10 });
|
|
246
|
+
histogram.enable();
|
|
247
|
+
const gauges = {
|
|
248
|
+
min: meter.createObservableGauge("nodejs.eventloop.delay.min", {
|
|
249
|
+
description: "Minimum event loop delay since the last export",
|
|
250
|
+
unit: "s",
|
|
251
|
+
}),
|
|
252
|
+
max: meter.createObservableGauge("nodejs.eventloop.delay.max", {
|
|
253
|
+
description: "Maximum event loop delay since the last export: the longest stall",
|
|
254
|
+
unit: "s",
|
|
255
|
+
}),
|
|
256
|
+
mean: meter.createObservableGauge("nodejs.eventloop.delay.mean", {
|
|
257
|
+
description: "Mean event loop delay since the last export",
|
|
258
|
+
unit: "s",
|
|
259
|
+
}),
|
|
260
|
+
p50: meter.createObservableGauge("nodejs.eventloop.delay.p50", {
|
|
261
|
+
description: "50th percentile event loop delay since the last export",
|
|
262
|
+
unit: "s",
|
|
263
|
+
}),
|
|
264
|
+
p90: meter.createObservableGauge("nodejs.eventloop.delay.p90", {
|
|
265
|
+
description: "90th percentile event loop delay since the last export",
|
|
266
|
+
unit: "s",
|
|
267
|
+
}),
|
|
268
|
+
p99: meter.createObservableGauge("nodejs.eventloop.delay.p99", {
|
|
269
|
+
description: "99th percentile event loop delay since the last export",
|
|
270
|
+
unit: "s",
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
const seconds = (ns: number) => ns / 1e9;
|
|
274
|
+
const collect = (observer: BatchObservableResult) => {
|
|
275
|
+
// No sample yet (first interval shorter than the resolution): the
|
|
276
|
+
// histogram's min is a sentinel then, so report nothing.
|
|
277
|
+
if (histogram.count === 0) return;
|
|
278
|
+
observer.observe(gauges.min, seconds(histogram.min));
|
|
279
|
+
observer.observe(gauges.max, seconds(histogram.max));
|
|
280
|
+
observer.observe(gauges.mean, seconds(histogram.mean));
|
|
281
|
+
observer.observe(gauges.p50, seconds(histogram.percentile(50)));
|
|
282
|
+
observer.observe(gauges.p90, seconds(histogram.percentile(90)));
|
|
283
|
+
observer.observe(gauges.p99, seconds(histogram.percentile(99)));
|
|
284
|
+
histogram.reset();
|
|
285
|
+
};
|
|
286
|
+
const observables = Object.values(gauges);
|
|
287
|
+
meter.addBatchObservableCallback(collect, observables);
|
|
288
|
+
stopEventLoopDelay = () => {
|
|
289
|
+
meter.removeBatchObservableCallback(collect, observables);
|
|
290
|
+
histogram.disable();
|
|
291
|
+
stopEventLoopDelay = undefined;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function stopEventLoopDelayMetrics(): void {
|
|
296
|
+
stopEventLoopDelay?.();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const CONSOLE_LOG_BODY_MAX_CHARS = 16_384;
|
|
300
|
+
const CONSOLE_SEVERITY = {
|
|
301
|
+
debug: { number: SeverityNumber.DEBUG, text: "DEBUG" },
|
|
302
|
+
log: { number: SeverityNumber.INFO, text: "INFO" },
|
|
303
|
+
info: { number: SeverityNumber.INFO, text: "INFO" },
|
|
304
|
+
warn: { number: SeverityNumber.WARN, text: "WARN" },
|
|
305
|
+
error: { number: SeverityNumber.ERROR, text: "ERROR" },
|
|
306
|
+
} as const;
|
|
307
|
+
type ConsoleMethod = keyof typeof CONSOLE_SEVERITY;
|
|
308
|
+
|
|
309
|
+
let restoreConsole: (() => void) | undefined;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Mirror console output into OTel log records (opt-in: OTEL_EXPORT_API_LOGS).
|
|
313
|
+
* stdout/stderr stay the primary sink; each line is also emitted to the
|
|
314
|
+
* global LoggerProvider, scrubbed at this egress point and correlated with
|
|
315
|
+
* the active span.
|
|
316
|
+
*/
|
|
317
|
+
export function installConsoleLogBridge(): void {
|
|
318
|
+
if (restoreConsole) return;
|
|
319
|
+
const logger = logs.getLogger(METER_NAME);
|
|
320
|
+
const originals = new Map<ConsoleMethod, (...args: unknown[]) => void>();
|
|
321
|
+
let emitting = false;
|
|
322
|
+
for (const method of Object.keys(CONSOLE_SEVERITY) as ConsoleMethod[]) {
|
|
323
|
+
const original = console[method].bind(console);
|
|
324
|
+
originals.set(method, console[method]);
|
|
325
|
+
console[method] = (...args: unknown[]) => {
|
|
326
|
+
original(...args);
|
|
327
|
+
// An exporter or scrubber that logs must not recurse into itself.
|
|
328
|
+
if (emitting) return;
|
|
329
|
+
emitting = true;
|
|
330
|
+
try {
|
|
331
|
+
// Scrub before truncating so a cut can never split a secret into an
|
|
332
|
+
// unrecognizable fragment.
|
|
333
|
+
const text = scrubSecrets(format(...args));
|
|
334
|
+
const body =
|
|
335
|
+
text.length > CONSOLE_LOG_BODY_MAX_CHARS
|
|
336
|
+
? `${text.slice(0, CONSOLE_LOG_BODY_MAX_CHARS)}…`
|
|
337
|
+
: text;
|
|
338
|
+
logger.emit({
|
|
339
|
+
severityNumber: CONSOLE_SEVERITY[method].number,
|
|
340
|
+
severityText: CONSOLE_SEVERITY[method].text,
|
|
341
|
+
body,
|
|
342
|
+
attributes: { "log.source": "console" },
|
|
343
|
+
});
|
|
344
|
+
} catch {
|
|
345
|
+
// Log export is best-effort; never break the caller's console call.
|
|
346
|
+
} finally {
|
|
347
|
+
emitting = false;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
restoreConsole = () => {
|
|
352
|
+
for (const [method, original] of originals) console[method] = original;
|
|
353
|
+
restoreConsole = undefined;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function uninstallConsoleLogBridge(): void {
|
|
358
|
+
restoreConsole?.();
|
|
209
359
|
}
|
|
210
360
|
|
|
211
361
|
export async function withSpan<T>(
|
package/src/otel.ts
CHANGED
|
@@ -94,18 +94,29 @@ export function isOtelEnabled(): boolean {
|
|
|
94
94
|
return otelConfigured();
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
const v = (process.env
|
|
97
|
+
function envFlagEnabled(name: string): boolean {
|
|
98
|
+
const v = (process.env[name] ?? "").trim().toLowerCase();
|
|
99
99
|
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
export function isPollTracingEnabled(): boolean {
|
|
103
|
+
return envFlagEnabled("OTEL_TRACE_POLL");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Mirror the API's console output to the OTLP collector as log records. Off by default. */
|
|
107
|
+
export function isApiLogExportEnabled(): boolean {
|
|
108
|
+
return envFlagEnabled("OTEL_EXPORT_API_LOGS");
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
export async function initOtel(serviceRole = process.env.AGENT_ROLE || "api"): Promise<void> {
|
|
103
112
|
if (!otelConfigured() || initialized) return;
|
|
104
113
|
initialized = true;
|
|
105
114
|
|
|
106
115
|
try {
|
|
107
116
|
const impl = await import("./otel-impl");
|
|
108
|
-
await impl.boot(serviceRole
|
|
117
|
+
await impl.boot(serviceRole, {
|
|
118
|
+
exportConsoleLogs: serviceRole === "api" && isApiLogExportEnabled(),
|
|
119
|
+
});
|
|
109
120
|
realWithSpan = impl.withSpan;
|
|
110
121
|
realStartSpan = impl.startSpan;
|
|
111
122
|
realWithRemoteContext = impl.withRemoteContext;
|
package/src/prompts/memories.ts
CHANGED
|
@@ -18,9 +18,26 @@ export type RelevantMemory = {
|
|
|
18
18
|
name: string;
|
|
19
19
|
content: string;
|
|
20
20
|
similarity: number;
|
|
21
|
+
rawSimilarity?: number;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Minimum pre-boost relevance for a memory to be injected into a task prompt.
|
|
26
|
+
*
|
|
27
|
+
* Compared against `rawSimilarity`, the [0,1] match score every retrieval arm
|
|
28
|
+
* emits before rerank (vec cosine, hybrid fused cosine, graph-derived), never
|
|
29
|
+
* against the composite `similarity`: the reranker's access, source-quality
|
|
30
|
+
* and usefulness multipliers can lift a score up to 4.5x, so a composite
|
|
31
|
+
* threshold admits unrelated rows. 0.55 sits between an unrelated query's best
|
|
32
|
+
* cosine (~0.33 on the production embedding model) and an exact-name hybrid
|
|
33
|
+
* hit (~0.65 fused).
|
|
34
|
+
*/
|
|
35
|
+
export const SIMILARITY_THRESHOLD = 0.55;
|
|
36
|
+
|
|
37
|
+
/** The score the injection threshold applies to. Older servers omit `rawSimilarity`. */
|
|
38
|
+
export function memoryRelevance(memory: { similarity: number; rawSimilarity?: number }): number {
|
|
39
|
+
return memory.rawSimilarity ?? memory.similarity;
|
|
40
|
+
}
|
|
24
41
|
|
|
25
42
|
const RATE_TOOL_HINT = `
|
|
26
43
|
|
|
@@ -35,7 +52,7 @@ This trains the swarm to surface better memories next time. Use sparingly:
|
|
|
35
52
|
* append entirely (matching pre-step-5 behaviour).
|
|
36
53
|
*/
|
|
37
54
|
export function renderMemoriesPrompt(memories: RelevantMemory[]): string | null {
|
|
38
|
-
const useful = memories.filter((m) => m
|
|
55
|
+
const useful = memories.filter((m) => memoryRelevance(m) > SIMILARITY_THRESHOLD);
|
|
39
56
|
if (useful.length === 0) return null;
|
|
40
57
|
|
|
41
58
|
const memoryContext = useful
|
|
@@ -31,6 +31,7 @@ export const CLAUDE_MANAGED_MODELS = [
|
|
|
31
31
|
"claude-opus-5",
|
|
32
32
|
"claude-fable-5",
|
|
33
33
|
"claude-mythos-5",
|
|
34
|
+
"claude-sonnet-5-5",
|
|
34
35
|
"claude-sonnet-5",
|
|
35
36
|
"claude-sonnet-4-6",
|
|
36
37
|
"claude-opus-4-8",
|
|
@@ -72,6 +73,7 @@ export interface ClaudeManagedModelPricing extends ClaudeManagedTokenPricing {
|
|
|
72
73
|
* - claude-mythos-5: $10 / $50 / $1.00 / $12.50 (limited availability, verified 2026-06-10)
|
|
73
74
|
* - claude-opus-5-5: $4 / $20 / $0.20 / $5.00 (verified 2026-09-22)
|
|
74
75
|
* - claude-opus-5: $5 / $25 / $0.50 / $6.25 (verified 2026-07-25)
|
|
76
|
+
* - claude-sonnet-5-5: $2 / $10 / $0.20 / $2.50 (verified 2026-09-28)
|
|
75
77
|
* - claude-sonnet-5: $2 / $10 / $0.20 / $2.50 (standard rate, verified 2026-08-29)
|
|
76
78
|
* - claude-sonnet-4-6: $3 / $15 / $0.30 / $3.75 (in / out / cache-read / cache-write)
|
|
77
79
|
* - claude-opus-4-8: $5 / $25 / $0.50 / $6.25 (verified 2026-05-28)
|
|
@@ -116,6 +118,12 @@ export const CLAUDE_MANAGED_MODEL_PRICING: Record<ClaudeManagedModel, ClaudeMana
|
|
|
116
118
|
cacheReadPerMillion: 1.0,
|
|
117
119
|
cacheWritePerMillion: 12.5,
|
|
118
120
|
},
|
|
121
|
+
"claude-sonnet-5-5": {
|
|
122
|
+
inputPerMillion: 2.0,
|
|
123
|
+
outputPerMillion: 10.0,
|
|
124
|
+
cacheReadPerMillion: 0.2,
|
|
125
|
+
cacheWritePerMillion: 2.5,
|
|
126
|
+
},
|
|
119
127
|
"claude-sonnet-5": {
|
|
120
128
|
inputPerMillion: 2.0,
|
|
121
129
|
outputPerMillion: 10.0,
|
|
@@ -21,32 +21,121 @@
|
|
|
21
21
|
* instructions.
|
|
22
22
|
* - Otherwise, write just the block.
|
|
23
23
|
* - Mark `createdFresh: true` so cleanup removes the file entirely.
|
|
24
|
+
* - `AGENTS.md` is a symlink (many repos ship `AGENTS.md -> CLAUDE.md`):
|
|
25
|
+
* never write through it. Swap in a real file holding the block plus the
|
|
26
|
+
* link target's contents, and restore the symlink on cleanup. The target is
|
|
27
|
+
* only read, and edits only carried back to it, when its real path is a
|
|
28
|
+
* regular file inside the cwd. A crash-recovery record (cwd + link target)
|
|
29
|
+
* is kept OUTSIDE the repo, so the next session can restore the link if
|
|
30
|
+
* this one crashed before cleanup. Repo content never names a path.
|
|
24
31
|
* - Existing `AGENTS.md` already contains the block: replace the block with
|
|
25
32
|
* the fresh contents.
|
|
26
33
|
* - Existing `AGENTS.md` without the block: prepend the block.
|
|
27
34
|
*
|
|
28
35
|
* Cleanup mirrors the creation logic — if we created the file fresh, delete
|
|
29
|
-
* it;
|
|
30
|
-
* block so anything the agent appended
|
|
36
|
+
* it; if we swapped out a symlink, restore it; otherwise re-read the current
|
|
37
|
+
* AGENTS.md and strip just the managed block so anything the agent appended
|
|
38
|
+
* during the session is preserved.
|
|
31
39
|
*
|
|
32
40
|
* The helper is deliberately isolated from the adapter so it can be
|
|
33
41
|
* unit-tested without pulling in the Codex SDK.
|
|
34
42
|
*/
|
|
35
43
|
|
|
36
|
-
import {
|
|
44
|
+
import { lstat, mkdir, readlink, realpath, symlink, unlink } from "node:fs/promises";
|
|
45
|
+
import { homedir } from "node:os";
|
|
46
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
37
47
|
|
|
38
48
|
const BLOCK_OPEN = "<swarm_system_prompt>";
|
|
39
49
|
const BLOCK_CLOSE = "</swarm_system_prompt>";
|
|
40
50
|
const BLOCK_REGEX = /<swarm_system_prompt>[\s\S]*?<\/swarm_system_prompt>\n?/;
|
|
51
|
+
const DEFAULT_STATE_DIR = join(homedir(), ".cache", "agent-swarm", "codex-agents-md");
|
|
41
52
|
|
|
42
53
|
export interface CodexAgentsMdHandle {
|
|
43
54
|
cleanup(): Promise<void>;
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
export interface CodexAgentsMdOptions {
|
|
58
|
+
/** Where symlink crash-recovery records live. Must be outside any repo. */
|
|
59
|
+
stateDir?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface SymlinkRecord {
|
|
63
|
+
cwd: string;
|
|
64
|
+
linkTarget: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
const NOOP_HANDLE: CodexAgentsMdHandle = {
|
|
47
68
|
cleanup: async () => {},
|
|
48
69
|
};
|
|
49
70
|
|
|
71
|
+
async function readIfExists(path: string): Promise<string | null> {
|
|
72
|
+
const file = Bun.file(path);
|
|
73
|
+
return (await file.exists()) ? await file.text() : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function recordPathFor(stateDir: string, realCwd: string): string {
|
|
77
|
+
const key = new Bun.CryptoHasher("sha256").update(realCwd).digest("hex");
|
|
78
|
+
return join(stateDir, `${key}.json`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readRecord(recordPath: string, realCwd: string): Promise<SymlinkRecord | null> {
|
|
82
|
+
try {
|
|
83
|
+
const record = JSON.parse(await Bun.file(recordPath).text()) as Partial<SymlinkRecord>;
|
|
84
|
+
if (record.cwd === realCwd && typeof record.linkTarget === "string") {
|
|
85
|
+
return { cwd: record.cwd, linkTarget: record.linkTarget };
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Missing or unreadable record: nothing to recover.
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Real path of the symlink target when it is a regular file inside the cwd,
|
|
95
|
+
* otherwise null. Out-of-tree and dangling targets are never read or written.
|
|
96
|
+
*/
|
|
97
|
+
async function inTreeTarget(
|
|
98
|
+
realCwd: string,
|
|
99
|
+
agentsMdPath: string,
|
|
100
|
+
linkTarget: string,
|
|
101
|
+
): Promise<string | null> {
|
|
102
|
+
const target = await realpath(resolve(dirname(agentsMdPath), linkTarget)).catch(() => null);
|
|
103
|
+
const prefix = realCwd.endsWith(sep) ? realCwd : `${realCwd}${sep}`;
|
|
104
|
+
if (target === null || !target.startsWith(prefix)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const stat = await lstat(target).catch(() => null);
|
|
108
|
+
return stat?.isFile() ? target : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Put the `AGENTS.md -> linkTarget` symlink back in place of our swapped-in
|
|
113
|
+
* real file. Edits the agent made to AGENTS.md during the session would have
|
|
114
|
+
* landed in the link target had the link been in place, so carry them over
|
|
115
|
+
* (block stripped) when the target is in-tree and its contents differ.
|
|
116
|
+
*/
|
|
117
|
+
async function restoreSymlink(
|
|
118
|
+
agentsMdPath: string,
|
|
119
|
+
realCwd: string,
|
|
120
|
+
linkTarget: string,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const stat = await lstat(agentsMdPath).catch(() => null);
|
|
123
|
+
const current = stat?.isFile() ? await Bun.file(agentsMdPath).text() : null;
|
|
124
|
+
await unlink(agentsMdPath).catch(() => {});
|
|
125
|
+
await symlink(linkTarget, agentsMdPath);
|
|
126
|
+
if (current === null) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const target = await inTreeTarget(realCwd, agentsMdPath, linkTarget);
|
|
130
|
+
if (target === null) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const stripped = current.replace(BLOCK_REGEX, "");
|
|
134
|
+
if (stripped !== (await Bun.file(target).text())) {
|
|
135
|
+
await Bun.write(target, stripped);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
50
139
|
/**
|
|
51
140
|
* Write (or refresh) a managed `<swarm_system_prompt>` block inside
|
|
52
141
|
* `${cwd}/AGENTS.md`. Returns a handle whose `cleanup()` reverses the edit.
|
|
@@ -56,6 +145,7 @@ const NOOP_HANDLE: CodexAgentsMdHandle = {
|
|
|
56
145
|
export async function writeCodexAgentsMd(
|
|
57
146
|
cwd: string | undefined,
|
|
58
147
|
systemPrompt: string | undefined,
|
|
148
|
+
options: CodexAgentsMdOptions = {},
|
|
59
149
|
): Promise<CodexAgentsMdHandle> {
|
|
60
150
|
if (!cwd || !systemPrompt) {
|
|
61
151
|
return NOOP_HANDLE;
|
|
@@ -63,33 +153,66 @@ export async function writeCodexAgentsMd(
|
|
|
63
153
|
|
|
64
154
|
const agentsMdPath = join(cwd, "AGENTS.md");
|
|
65
155
|
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
156
|
+
const stateDir = options.stateDir ?? DEFAULT_STATE_DIR;
|
|
157
|
+
const realCwd = await realpath(cwd).catch(() => resolve(cwd));
|
|
158
|
+
const recordPath = recordPathFor(stateDir, realCwd);
|
|
66
159
|
const block = `${BLOCK_OPEN}\n${systemPrompt}\n${BLOCK_CLOSE}`;
|
|
67
160
|
|
|
68
|
-
|
|
69
|
-
|
|
161
|
+
let stat = await lstat(agentsMdPath).catch(() => null);
|
|
162
|
+
|
|
163
|
+
// A previous session swapped a symlink out and crashed before cleanup. Only
|
|
164
|
+
// our own out-of-repo record can trigger a restore, and only while AGENTS.md
|
|
165
|
+
// is still the real file holding our block.
|
|
166
|
+
const record = await readRecord(recordPath, realCwd);
|
|
167
|
+
if (record) {
|
|
168
|
+
if (stat?.isFile() && BLOCK_REGEX.test(await Bun.file(agentsMdPath).text())) {
|
|
169
|
+
await restoreSymlink(agentsMdPath, realCwd, record.linkTarget);
|
|
170
|
+
stat = await lstat(agentsMdPath);
|
|
171
|
+
}
|
|
172
|
+
await unlink(recordPath).catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (stat?.isSymbolicLink()) {
|
|
176
|
+
const linkTarget = await readlink(agentsMdPath);
|
|
177
|
+
const target = await inTreeTarget(realCwd, agentsMdPath, linkTarget);
|
|
178
|
+
const targetContent = target !== null ? await Bun.file(target).text() : "";
|
|
179
|
+
// Record before swapping, so a crash at any later point stays recoverable.
|
|
180
|
+
await mkdir(stateDir, { recursive: true });
|
|
181
|
+
await Bun.write(recordPath, JSON.stringify({ cwd: realCwd, linkTarget }));
|
|
182
|
+
// Never write through the link: replace it with a real file for the session.
|
|
183
|
+
await unlink(agentsMdPath);
|
|
184
|
+
await Bun.write(agentsMdPath, `${block}\n${targetContent.replace(BLOCK_REGEX, "")}`);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
async cleanup(): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
await restoreSymlink(agentsMdPath, realCwd, linkTarget);
|
|
190
|
+
await unlink(recordPath).catch(() => {});
|
|
191
|
+
} catch {
|
|
192
|
+
// Cleanup is best-effort; the record lets the next session finish
|
|
193
|
+
// the restore.
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
70
198
|
|
|
71
199
|
let createdFresh = false;
|
|
72
200
|
let newContent: string;
|
|
73
201
|
|
|
74
|
-
if (!
|
|
202
|
+
if (!stat) {
|
|
75
203
|
// No AGENTS.md yet — prefer CLAUDE.md content as a base if present.
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
if (claudeMdExists) {
|
|
79
|
-
const claudeContent = await claudeMdFile.text();
|
|
80
|
-
newContent = `${block}\n\n${claudeContent}`;
|
|
81
|
-
} else {
|
|
82
|
-
newContent = `${block}\n`;
|
|
83
|
-
}
|
|
204
|
+
const claudeContent = await readIfExists(claudeMdPath);
|
|
205
|
+
newContent = claudeContent !== null ? `${block}\n\n${claudeContent}` : `${block}\n`;
|
|
84
206
|
createdFresh = true;
|
|
85
207
|
} else {
|
|
86
|
-
const existingContent = await
|
|
208
|
+
const existingContent = await Bun.file(agentsMdPath).text();
|
|
87
209
|
if (BLOCK_REGEX.test(existingContent)) {
|
|
88
210
|
// Replace the stale block in place.
|
|
89
211
|
newContent = existingContent.replace(BLOCK_REGEX, `${block}\n`);
|
|
90
212
|
} else {
|
|
91
|
-
// Prepend the block
|
|
92
|
-
|
|
213
|
+
// Prepend the block with a single newline so cleanup's strip restores
|
|
214
|
+
// the original bytes exactly.
|
|
215
|
+
newContent = `${block}\n${existingContent}`;
|
|
93
216
|
}
|
|
94
217
|
}
|
|
95
218
|
|