From 53fff54c847722a10241060d172c36f78e12f115 Mon Sep 17 00:00:00 2001 From: weishu Date: Mon, 19 Jan 2026 13:07:25 +0800 Subject: [PATCH] Release version 0.10.0 --- bun.lock | 22 +++-- cli/package.json | 12 +-- cli/src/api/socketOutbox.ts | 156 ++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 cli/src/api/socketOutbox.ts diff --git a/bun.lock b/bun.lock index 8a486ce9..47b2ada9 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ }, "cli": { "name": "@twsxtd/hapi", - "version": "0.9.2", + "version": "0.10.0", "bin": { "hapi": "bin/hapi.cjs", }, @@ -43,11 +43,11 @@ "vitest": "^4.0.16", }, "optionalDependencies": { - "@twsxtd/hapi-darwin-arm64": "0.9.2", - "@twsxtd/hapi-darwin-x64": "0.9.2", - "@twsxtd/hapi-linux-arm64": "0.9.2", - "@twsxtd/hapi-linux-x64": "0.9.2", - "@twsxtd/hapi-win32-x64": "0.9.2", + "@twsxtd/hapi-darwin-arm64": "0.10.0", + "@twsxtd/hapi-darwin-x64": "0.10.0", + "@twsxtd/hapi-linux-arm64": "0.10.0", + "@twsxtd/hapi-linux-x64": "0.10.0", + "@twsxtd/hapi-win32-x64": "0.10.0", }, }, "docs": { @@ -837,15 +837,13 @@ "@twsxtd/hapi": ["@twsxtd/hapi@workspace:cli"], - "@twsxtd/hapi-darwin-arm64": ["@twsxtd/hapi-darwin-arm64@0.9.2", "", { "os": "darwin", "cpu": "arm64", "bin": { "hapi": "bin/hapi" } }, "sha512-D69c50cu5BSTVR1Onxo6evLqplMhsuTtwaxjs38enX8RP5vfDKQMG33XHEnIODDNIiOQpTO72I8kGoKF/q+lVQ=="], + "@twsxtd/hapi-darwin-arm64": ["@twsxtd/hapi-darwin-arm64@0.10.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "hapi": "bin/hapi" } }, "sha512-XRnxVmgBGCCRMp60ZCJvlrh0ZoE/kUIywNM4x76vqJaaJlq0Yp0FNUZLkOAzQhZCmfmijVCW/Ka6o844gPHOvg=="], - "@twsxtd/hapi-darwin-x64": ["@twsxtd/hapi-darwin-x64@0.9.2", "", { "os": "darwin", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-21oQSD85UZCPkJy5NQ8MB3FStVrxiIUw3erTTBLTepmdWDSojkrqvsWUAyRKGckrpwkn1+dUjJ0bTmy5pCT/SA=="], + "@twsxtd/hapi-darwin-x64": ["@twsxtd/hapi-darwin-x64@0.10.0", "", { "os": "darwin", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-XvKEpwW1Z1AcbyNYCz/N+Xfv6obuO5ru/6Hr1nT/NzU5Z6vG0qfF2pLzWkbEtRQaT036i4zTB5a1G8iJYHfp2w=="], - "@twsxtd/hapi-linux-arm64": ["@twsxtd/hapi-linux-arm64@0.9.2", "", { "os": "linux", "cpu": "arm64", "bin": { "hapi": "bin/hapi" } }, "sha512-/GKrNvFqEHmfYWkfBcIjh/AD2HXZdqObsLClOqdFEmnDGq1WS1RpC/cJi5wMi2yLZE2JW6V3GVGWQi2qE5a7cA=="], + "@twsxtd/hapi-linux-arm64": ["@twsxtd/hapi-linux-arm64@0.10.0", "", { "os": "linux", "cpu": "arm64", "bin": { "hapi": "bin/hapi" } }, "sha512-XvovcoaOZWq6n8wfAilg8loqTwrIY7QNFyxQlsURNJgMsd3iXDT/VysACd481xOqWEnMxOSrhA9GERCDjBxhYw=="], - "@twsxtd/hapi-linux-x64": ["@twsxtd/hapi-linux-x64@0.9.2", "", { "os": "linux", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-784+qw88QqtgJHU9mho2nfMRCdozvX8sSBvHZHl9Jzy7XStzHzap6jlBeGX8JCSh5j33AmNae9YMBTm2AyX/6w=="], - - "@twsxtd/hapi-win32-x64": ["@twsxtd/hapi-win32-x64@0.9.2", "", { "os": "win32", "cpu": "x64", "bin": { "hapi": "bin/hapi.exe" } }, "sha512-Rf8lMb0sltI51tlUXKCz32F6w9QEMgkoAiF5scGsSwDvrr4/rkAy7UAunUQzFlZCWnGGziY29e28fHdnCkOi9g=="], + "@twsxtd/hapi-linux-x64": ["@twsxtd/hapi-linux-x64@0.10.0", "", { "os": "linux", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-TU6pB/XMz4xVW9LwZqA8sRANd8RK8qrm7jFp7cANuS+L/66J4VGuGmi1cziFhVLleyqqur7fh2J/xJtkY5Cb5w=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], diff --git a/cli/package.json b/cli/package.json index 5a2085ab..07aa03e8 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@twsxtd/hapi", - "version": "0.9.2", + "version": "0.10.0", "description": "App for agentic coding - access coding agent anywhere", "author": "Kirill Dubovitskiy & weishu", "license": "AGPL-3.0-only", @@ -26,11 +26,11 @@ } }, "optionalDependencies": { - "@twsxtd/hapi-darwin-arm64": "0.9.2", - "@twsxtd/hapi-darwin-x64": "0.9.2", - "@twsxtd/hapi-linux-arm64": "0.9.2", - "@twsxtd/hapi-linux-x64": "0.9.2", - "@twsxtd/hapi-win32-x64": "0.9.2" + "@twsxtd/hapi-darwin-arm64": "0.10.0", + "@twsxtd/hapi-darwin-x64": "0.10.0", + "@twsxtd/hapi-linux-arm64": "0.10.0", + "@twsxtd/hapi-linux-x64": "0.10.0", + "@twsxtd/hapi-win32-x64": "0.10.0" }, "scripts": { "postinstall": "node -e \"try{require('fs').chmodSync(require('path').join(__dirname,'bin','hapi.cjs'),0o755)}catch(e){}\"", diff --git a/cli/src/api/socketOutbox.ts b/cli/src/api/socketOutbox.ts new file mode 100644 index 00000000..6ae92182 --- /dev/null +++ b/cli/src/api/socketOutbox.ts @@ -0,0 +1,156 @@ +import { logger } from '@/ui/logger' + +const DEFAULT_OUTBOX_MAX_BYTES = resolveEnvNumber('HAPI_OUTBOX_MAX_BYTES', 16_000_000) +const DEFAULT_OUTBOX_MAX_ITEMS = resolveEnvNumber('HAPI_OUTBOX_MAX_ITEMS', 500) +const DEFAULT_OUTBOX_MAX_ITEM_BYTES = resolveEnvNumber('HAPI_OUTBOX_MAX_ITEM_BYTES', 1_000_000) +const DEFAULT_OUTBOX_MAX_AGE_MS = resolveEnvNumber('HAPI_OUTBOX_MAX_AGE_MS', 15 * 60_000, true) +const DEFAULT_DROP_LOG_INTERVAL_MS = resolveEnvNumber('HAPI_OUTBOX_DROP_LOG_INTERVAL_MS', 5_000) + +type OutboxItem = { + event: string + args: readonly unknown[] + sizeBytes: number + enqueuedAt: number +} + +type SocketOutboxOptions = { + maxBytes?: number + maxItems?: number + maxItemBytes?: number + maxAgeMs?: number + dropLogIntervalMs?: number +} + +function resolveEnvNumber(name: string, fallback: number, allowZero: boolean = false): number { + const raw = process.env[name] + if (!raw) { + return fallback + } + const parsed = Number.parseInt(raw, 10) + if (!Number.isFinite(parsed)) { + return fallback + } + if (parsed < 0) { + return fallback + } + if (parsed === 0 && !allowZero) { + return fallback + } + return parsed +} + +function estimateSizeBytes(payload: unknown): number { + try { + return Buffer.byteLength(JSON.stringify(payload), 'utf8') + } catch { + return Number.MAX_SAFE_INTEGER + } +} + +export class SocketOutbox { + private readonly maxBytes: number + private readonly maxItems: number + private readonly maxItemBytes: number + private readonly maxAgeMs: number + private readonly dropLogIntervalMs: number + private items: OutboxItem[] = [] + private queuedBytes = 0 + private droppedCount = 0 + private droppedBytes = 0 + private lastDropLogAt = 0 + private lastDropReason = 'unknown' + + constructor(options?: SocketOutboxOptions) { + this.maxBytes = options?.maxBytes ?? DEFAULT_OUTBOX_MAX_BYTES + this.maxItems = options?.maxItems ?? DEFAULT_OUTBOX_MAX_ITEMS + this.maxItemBytes = options?.maxItemBytes ?? DEFAULT_OUTBOX_MAX_ITEM_BYTES + this.maxAgeMs = options?.maxAgeMs ?? DEFAULT_OUTBOX_MAX_AGE_MS + this.dropLogIntervalMs = options?.dropLogIntervalMs ?? DEFAULT_DROP_LOG_INTERVAL_MS + } + + enqueue(event: string, args: readonly unknown[]): boolean { + if (this.maxBytes <= 0 || this.maxItems <= 0) { + this.recordDrop('outbox-disabled', 0) + return false + } + + this.dropExpired() + + const sizeBytes = estimateSizeBytes({ event, args }) + if (sizeBytes > this.maxItemBytes || sizeBytes > this.maxBytes) { + this.recordDrop('item-too-large', sizeBytes) + return false + } + + while (this.items.length >= this.maxItems || this.queuedBytes + sizeBytes > this.maxBytes) { + const removed = this.items.shift() + if (!removed) { + break + } + this.queuedBytes -= removed.sizeBytes + this.recordDrop('outbox-full', removed.sizeBytes) + } + + if (this.items.length >= this.maxItems || this.queuedBytes + sizeBytes > this.maxBytes) { + this.recordDrop('outbox-full', sizeBytes) + return false + } + + this.items.push({ + event, + args, + sizeBytes, + enqueuedAt: Date.now() + }) + this.queuedBytes += sizeBytes + return true + } + + flush(emit: (event: string, args: readonly unknown[]) => void): void { + this.dropExpired() + + if (this.items.length === 0) { + return + } + + const items = this.items + this.items = [] + this.queuedBytes = 0 + + for (const item of items) { + emit(item.event, item.args) + } + } + + private dropExpired(): void { + if (this.maxAgeMs <= 0) { + return + } + + const cutoff = Date.now() - this.maxAgeMs + while (this.items.length > 0 && this.items[0].enqueuedAt < cutoff) { + const removed = this.items.shift() + if (!removed) { + break + } + this.queuedBytes -= removed.sizeBytes + this.recordDrop('expired', removed.sizeBytes) + } + } + + private recordDrop(reason: string, bytes: number): void { + this.droppedCount += 1 + this.droppedBytes += bytes + this.lastDropReason = reason + + const now = Date.now() + if (now - this.lastDropLogAt < this.dropLogIntervalMs) { + return + } + + logger.warn(`[OUTBOX] Dropped ${this.droppedCount} items (${this.droppedBytes} bytes). reason=${this.lastDropReason}`) + this.droppedCount = 0 + this.droppedBytes = 0 + this.lastDropLogAt = now + } +}