feat: client portal + Bing Ads pipeline - #2
Closed
clicktoacquire wants to merge 299 commits into
Closed
clicktoacquire wants to merge 299 commits into
clicktoacquire wants to merge 299 commits into
Conversation
…um#117) validateOrgName enforced lowercase which rejects mixed-case org names (e.g. AcmeCorp) that predate the strict lowercase policy. The env resolution layer should not re-validate what the filesystem already accepted at org-creation time. Restrict to unsafe-character rejection (path traversal, whitespace) only. Lowercase enforcement stays at the CLI --org parse layer where user input is normalised before use. Fixes bus commands that fail with "invalid org" after upstream added strict validateOrgName to resolveEnv, even for org directories that exist on disk with mixed casing. Co-authored-by: Bob <bob@agents.cortextos>
…sh alert (grandamenium#109) The SessionEnd crash-alert hook currently pages Telegram on every non-crash end type (planned-restart, session-refresh, daemon-stop, user-*) regardless of time, and has no deduplication. A single misclassified crash loop (e.g. the Claude Code CLI weekly-limit wall, which surfaces as "You've used 100% of your weekly limit · resets Xpm" rather than an API error body) results in a 🚨 CRASH Telegram alert every 30 minutes all night. This changes the hook to: 1. Detect Anthropic rate-limit and CLI weekly-limit signatures in the tail of stdout.log and reclassify bare `crash` exits as a new `rate-limited` end type. The calmer ⏳ message is emitted instead of 🚨 CRASH, and the exit is subject to quiet hours. Patterns scanned: overloaded_error, rate_limit_error, rate limit, too many requests, quota exceeded, usage limit, weekly limit, 5-hour limit, and the Claude Code CLI status-bar regex /used \d+% of your/. 2. Apply quiet hours (22:00–07:00 America/Los_Angeles) to every routine end type: planned-restart, session-refresh, daemon-stop, user-*, and rate-limited. Real unexpected `crash` events still page at night — that is the only thing worth waking the operator for. 3. Deduplicate identical (agent, type) alerts within a 10-minute window via a .crash_alert_dedup.json state file. A broken watchdog loop produces at most one notification instead of a buzz storm. All events continue to be appended to crashes.log regardless of whether the Telegram alert is actually sent, so post-hoc visibility is preserved. The quiet-hour window and dedup TTL are defined as module constants (QUIET_HOUR_START_LA = 22, QUIET_HOUR_END_LA = 7, DEDUP_WINDOW_MS = 10 minutes) so they are easy to adjust. Timezone resolution uses Intl via toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }), which does not depend on the host TZ. Tested against cortextos on revops-global — stopped a 7×30-minute overnight alert loop on an agent that had hit the Claude weekly limit.
…randamenium#123) Community-contributed skill belongs in community/skills/ not templates/agent/.claude/skills/ per project policy. Original PR: grandamenium#92 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Community-contributed skill belongs in community/skills/ not templates/agent/.claude/skills/ per project policy. Original PR: grandamenium#93 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Community-contributed skill belongs in community/skills/ not templates/agent/.claude/skills/ per project policy. Original PR: grandamenium#95 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Community-contributed skill belongs in community/skills/ not templates/agent/.claude/skills/ per project policy. Original PR: grandamenium#96 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… template (grandamenium#127) Community-contributed skill belongs in community/skills/ per policy. As an M2C1-related skill, also added to templates/m2c1-worker/.claude/skills/ (new m2c1-worker template directory, created as part of this fix). Original PR: grandamenium#97 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…andamenium#128) Adds mandatory planning phase (PLAN.md → send → await PLAN_APPROVED) before any source files are written, and a corresponding supervisor review checklist with approve/reject bus commands. Changes apply to templates/agent/.claude/skills/m2c1-worker/SKILL.md (existing template edit — no path change needed per policy). Also mirrored to new templates/m2c1-worker/.claude/skills/m2c1-worker/. Original PR: grandamenium#98 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nium#130) Community-contributed skill belongs in community/skills/ not templates/agent/.claude/skills/ per project policy. Original PR: grandamenium#102 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…randamenium#129) Adds self-monitoring stuck detection (5 consecutive identical tool calls triggers STUCK ALERT bus message to supervisor) and matching supervisor handling section with specific directive guidance. Changes apply to templates/agent/.claude/skills/m2c1-worker/SKILL.md (existing template edit — no path change needed per policy). Also mirrored to templates/m2c1-worker/.claude/skills/m2c1-worker/. Original PR: grandamenium#99 Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nsitive fs
existsSync('orgs/acmecorp') returns true on macOS/Windows even when
the directory was created as 'orgs/AcmeCorp', causing normalizeOrgName
to incorrectly return the input as-is instead of the canonical on-disk
casing. Replace the existsSync fast-path with readdirSync+entries.includes()
so the exact-case check operates on the actual directory listing rather
than the filesystem's case-folded lookup.
Also fix the case-sensitive-fs test to skip gracefully on macOS where
mkdirSync('acmecorp') throws EEXIST when 'AcmeCorp' already exists.
…ort (grandamenium#134) Both PreCompact hooks could hit the settings.json timeout (10s/15s) and cause Claude Code to abort compaction entirely, leaving agents stuck at 100% context with no recovery path. - hook-compact-telegram: add 5s AbortController to fetch() so the Telegram call always resolves before the 10s hook timeout fires - hook-extract-facts: race readStdin() against a 10s timer so the hook exits cleanly even if Claude Code does not close stdin, well within the 15s hook timeout Fixes the "no compaction message in 24h" regression introduced after grandamenium#116. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…emplate (grandamenium#137) * fix(hooks): remove hook-extract-facts from PreCompact — never worked, revert to simple notification hook-extract-facts assumed Claude Code sends a summary via stdin at PreCompact time. It does not — it sends transcript_path. The hook has never written a single facts file. Removing it from all agent and template settings.json files. PreCompact now has only hook-compact-telegram (with the 5s AbortController timeout from PR grandamenium#134). Simple and reliable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cli): add CLAUDE_CODE_DISABLE_1M_CONTEXT=true to new agent .env template Claude Code v2.1.111+ gives Sonnet 4.6 a 1M context window by default. Without extra usage billing enabled, compaction fails with: "Extra usage is required for 1M context" All agents need CLAUDE_CODE_DISABLE_1M_CONTEXT=true in their .env to revert to standard 200k context and keep auto-compaction working. Adding it to the generated .env so every new agent gets it automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cli): note Opus exception in CLAUDE_CODE_DISABLE_1M_CONTEXT comment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Approval-gated virtual Visa card issuance for autonomous agent purchases. Uses AgentCard.sh MCP tools for card creation, checkout, and lifecycle. Depends on the approvals skill for human-in-the-loop authorization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… revert to simple notification (grandamenium#135) hook-extract-facts assumed Claude Code sends a summary via stdin at PreCompact time. It does not — it sends transcript_path. The hook has never written a single facts file. Removing it from all agent and template settings.json files. PreCompact now has only hook-compact-telegram (with the 5s AbortController timeout from PR grandamenium#134). Simple and reliable. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ses wrong URL (grandamenium#139) * fix(dashboard): org filter not applied on page load; login redirect uses wrong URL - DashboardShell restores org from localStorage on mount but never navigates to sync the URL, so the server renders with all agents while the selector shows the saved org. Add a mount-only useEffect that calls router.replace() when the saved org differs from the current ?org= param. - login/page.tsx: after a successful redirect:follow sign-in, navigate to '/' instead of res.url. res.url resolves to an absolute URL which can be http://localhost:3000/ behind a reverse proxy, causing navigation to the wrong host. '/' always resolves to the current origin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dashboard): address PR review — URL authoritative for org; preserve callbackUrl dashboard-shell.tsx: Remove the mount-only useEffect that synced localStorage to the URL. That approach clobbered deep links with explicit ?org= params, caused a double server render and visible flash, and introduced a race with OrgSelector. Instead, read window.location.search directly in the useState initializer so the URL is authoritative at hydration time, with localStorage as fallback. No navigation side effect needed. login/page.tsx: Hardcoding '/' lost the callbackUrl set by middleware when bouncing unauthenticated users to login. Read callbackUrl from the current URL's search params and validate it is a same-origin path (starts with / but not //) before navigating. Falls back to / if absent or invalid, which also guards against open-redirect via callbackUrl=//evil.com. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a Telegram message is delivered to an agent, prepend the last 6 back-and-forth messages (inbound + outbound, sorted by timestamp) as a [Recent conversation:] block. This gives the agent visibility into what was said immediately before the current message, eliminating the context gap when multiple senders (director, analyst, etc.) write to the same chat and the human replies. - logging.ts: add buildRecentHistory() reads inbound/outbound JSONL logs - fast-checker.ts: add optional recentHistory param to formatTelegramTextMessage - agent-manager.ts: call buildRecentHistory and pass to formatter
…log.json churn Two fixes applied to PR grandamenium#140 (feat: recent conversation history): 1. logging.ts: replace hardcoded 'Greg' speaker label with process.env.ADMIN_USERNAME ?? 'user'. The original used a personal name that would appear verbatim in every cortextOS installation. 2. community/catalog.json: revert the cosmetic reformatter churn (em-dash → \u2014, inline tags → multi-line). That change was unrelated to the feature and inflated the diff by 150 lines. The feature itself (buildRecentHistory, formatTelegramTextMessage injection, agent-manager wiring) is preserved as-is from the original PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enium#121) Adds two files: - scripts/setup-hooks.sh: one-time setup script. Run after cloning to install git hooks into .git/hooks/. - scripts/hooks/pre-push: pre-push hook that runs npm run build && npm test before any push. Aborts the push if either step fails. Motivation: two CI failures overnight were caused by TypeScript errors and missing dashboard deps that were not caught locally before pushing. James directive: fail locally, not on CI. Usage: bash scripts/setup-hooks.sh # run once after cloning Note: git hooks are not versioned (.git/hooks/ is not committed). setup-hooks.sh bridges this by copying the versioned hook sources into the local .git/hooks directory. Each contributor runs setup-hooks.sh once — CI is not affected. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…issue grandamenium#110) (grandamenium#120) Before this change, gap detection silently skipped any cron with no fire record in cron-state.json. On fresh deploy or after a daemon restart, all entries are missing, so the detection loop ran every 10 minutes but never fired a single nudge — creating guaranteed dead zones as long as the first cron fire had not yet been recorded. Fix: pass loopStartedAt (the timestamp when scheduleGapDetection was called) into runGapDetectionLoop. When no record exists for a cron, use loopStartedAt as the implicit last_fire. Gap detection then fires a nudge if the cron has not fired within 2x its interval AFTER the daemon started — directly addressing the cold-start dead-zone. Closes grandamenium#110. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions to the agent (grandamenium#141) Adds support for Telegram's message_reaction update type so agents can see when a user reacts to one of the bot's messages. Reactions are injected into the PTY as a one-line "=== REACTION from ... ===" banner so the agent can follow up on acks or clarify after a negative signal. Changes: 1. src/telegram/api.ts: append 'message_reaction' to allowed_updates in getUpdates. Without this Telegram filters reactions out server-side and the poller never sees them. 2. src/types/index.ts: new TelegramMessageReaction interface and TelegramReactionType tagged union (emoji | custom_emoji). Optional message_reaction field on TelegramUpdate. 3. src/telegram/poller.ts: new ReactionHandler type, onReaction() registrar, and pollOnce() routing with the same try/catch/handlerFailed + offset-after-handler contract as the existing message and callback_query paths. A reaction-handler throw leaves the update un-acknowledged so Telegram re-delivers it. 4. src/daemon/fast-checker.ts: new FastChecker.formatTelegramReaction() static formatter. Handles three shapes: added reaction (render new_reaction emojis), removed reaction (render "removed <old>"), custom_emoji placeholder (since we do not resolve custom_emoji_id). 5. src/daemon/agent-manager.ts: poller.onReaction() wire-in alongside the existing onMessage/onCallback handlers. Same ALLOWED_USER gate, same dedup via FastChecker.isDuplicate, same queueTelegramMessage PTY injection path — reactions flow through the same plumbing as text messages so they inherit rate limits and ordering. Tests: 2 new in tests/unit/telegram/poller.test.ts (route to handler + offset advance, handler-throw does not advance offset) + 4 in tests/unit/daemon/fast-checker.test.ts (single emoji, multiple concurrent emojis, removed reaction, custom_emoji placeholder). Full suite 664/664 green, tsc clean, build clean. Co-authored-by: Bob <bob@agents.cortextos>
…randamenium#142) The regex in the goals generate-md subcommand rejected any argument containing an uppercase character, which made it impossible to run `cortextos goals generate-md --agent <name> --org <CamelCaseOrg>` — the exact case for mixed-case org directories that predate the strict-lowercase policy. Fix: change `/^[a-z0-9_-]+$/` to `/^[A-Za-z0-9_-]+$/` on both the agent and org arguments. The rest of the validator (length, dash/underscore allowlist, rejection of path traversal or spaces) is unchanged — this relaxation is scoped to the case class. Matches the same fix already applied to src/utils/validate.ts. The goals subcommand has its own inline check and was missed by that earlier patch. Verified: `cortextos goals generate-md --agent <name> --org <CamelCaseOrg>` now returns "Generated GOALS.md for <name>" instead of the "alphanumeric/dash/underscore only" error. Full suite green, tsc clean, build clean. Co-authored-by: Bob <bob@agents.cortextos>
…ing global list (grandamenium#143) `cortextos bus manage-cycle list <agent>` was returning every cycle in config.json regardless of the agent argument. Cycle-to-agent attribution in storage was already correct (each cycle record carries its own `agent` field) — the bug was purely in the list path's ignoring the filter. Fix: in the `list` case of `manageCycle()`, filter `config.cycles` by `options.agent` when supplied. When the caller omits the agent, the full list is still returned so existing callers that deliberately want a global view keep working (back-compat). Adds one unit test in tests/sprint3-experiments.test.ts covering: two cycles under 'alice' + one under a second agent, list --agent=alice returns exactly the alice pair, list --agent=other-agent returns just the other cycle, list with no filter returns all three (back-compat). Full suite 665/665 green, tsc clean. Co-authored-by: Bob <bob@agents.cortextos>
…onnecting-IP fallback, rate-limit error message (grandamenium#144) Three fixes for issue grandamenium#138: 1. .env.local.example: add commented TRUST_PROXY and AUTH_URL entries with explanations so users know to set these when deploying behind Cloudflare Tunnel or any other reverse proxy. 2. auth.ts: fall back to CF-Connecting-IP header when TRUST_PROXY is unset. CF-Connecting-IP is injected by Cloudflare and not spoofable from outside Cloudflare's network, so it is safe to use without TRUST_PROXY=true. Prevents all login attempts from bucketing as 0.0.0.0 behind a quick tunnel. 3. login/page.tsx: map CallbackRouteError to a human-readable message. "Too many attempts. Please wait a few minutes and try again." instead of the raw NextAuth error code which gave no indication of the real cause. Closes grandamenium#138. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…-v5v3 + GHSA-458j-xx4x-4375 (grandamenium#146) REGRESSION: upstream merge a3002be overwrote the previous next@16.2.3 fix (577f16e), reverting dashboard to next@16.2.1 which is vulnerable to GHSA-q4gf-8mx6-v5v3 (Server Components DoS, HIGH). This commit: - Bumps next to 16.2.4 (exact pin, no caret — prevents future drift) - npm audit fix also bumps hono to >=4.12.14 clearing GHSA-458j-xx4x-4375 (JSX SSR HTML injection, MODERATE) - Post-fix: npm audit reports 0 vulnerabilities - 665/665 tests pass Co-authored-by: friday <ClintMoody@users.noreply.github.com>
…randamenium#147) Upstream merges can silently downgrade a security-patched dependency by carrying a pinned old version. Without an automated check, the regression goes unnoticed until the next manual audit. Fix: add `npm audit --audit-level=moderate` step to the upstream merge workflow, running AFTER npm install and BEFORE build/test. If audit reports any moderate+ vulnerability: block the merge, record advisory IDs + affected packages + severity, report to the orchestrator for manual resolution. Updated in 2 tracked files: - community/skills/framework-upstream-auto-update/SKILL.md - templates/analyst/.claude/skills/upstream-sync/SKILL.md FRIDAY's live copy (orgs/*/agents/friday/.claude/skills/upstream-sync) also updated locally but gitignored from the upstream repo. Co-authored-by: Bob <ClintMoody@users.noreply.github.com>
…with live logs and implementation plan
…oded-name-and-catalog-churn fix(pr140): replace hardcoded speaker name, revert catalog churn
…ict-org-case-sensitivity fix(org): use readdirSync for exact-case match — fixes macOS case-insensitive fs
…ession-restore fix(daemon): prevent duplicate crons on rapid session restarts
… macOS canonicalize fix
…amenium#563) * feat(usage-monitor): unified Claude Max + Codex usage tracking - bus/check-usage-api.sh: add Codex live usage % via chatgpt.com/backend-api/wham/usage - New _codex_wham_usage() fetches primary_window.used_percent (5h) and secondary_window.used_percent (7d) directly from OpenAI's undocumented wham/usage endpoint (found in Codex Rust binary) - Auto-refreshes Codex OAuth token from ~/.codex/auth.json when < 5 min remaining - Output now includes codex.utilization_5h, codex.utilization_7d, codex.limit_reached, codex.reset_5h_seconds, codex.reset_7d_seconds alongside existing SQLite token counts - Adds Codex threshold alerts (limit_reached, >=80% 7d, >=90% 5h) matching Claude Max - Codex wham/usage result cached 5 min at $CTX_ROOT/state/usage/codex-wham-cache.json - templates/analyst/config.json: add usage-monitor cron (2h interval) - Checks both Claude Max and Codex utilization on every fire - Alerts on new 10% thresholds, 80% 7d CODE RED, 90% 5h warning Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(usage-monitor): fix sprint1 test failures — bash ref + cron count - Replace `bash $CTX_FRAMEWORK_ROOT/bus/check-usage-api.sh` with `cortextos bus check-usage-api` in analyst template prompt - Update sprint1 cron count assertion: 5→6, add usage-monitor to expected names - Remove hardcoded --chat-id from template prompt (use env var default) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Boris <noreply@anthropic.com>
* feat: add voice-agent-factory community skill Turns any cortextOS agent into a live ElevenLabs voice agent. Six-phase pipeline: DISCOVER (mine skills/CLIs/MCPs/transcripts) -> ASK -> GENERATE (all code written dynamically per target, policy-gated gateway, server-side invariants) -> TEST (probe-shaped fixtures) -> PROVISION (tier-detect with server-tools fallback) -> VERIFY (real text-only WS conversations, never simulate-conversation which mocks tools). 14 hard-won lessons encoded from two live reference builds. Bundled resources: verified EL API reference, webhook schema + pricing gaps doc, sources. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(voice-agent-factory): address review round 1 — operator gate, MCP attach, runtime detection, fresh-user path - Hard operator gate before Phase 5: no EL provisioning, tunnel, or link sharing on defaults; Phase 2 defaults limited to discovery/generation - MCP path now PATCHes agent prompt mcp_server_ids after POST (append, preserve existing), matching the tool_ids discipline - Runtime detection in Phase 1 with codex-app-server alternate paths and an explicit unsupported-runtime stop instead of silent under-mining - Prerequisites / How to run / What this writes section (incl. unpkg CDN note and no-CDN variant) - GAPS.md: body_params_schema -> request_body_schema (+ response_body_schema docs-only note); REPORT section 8 reconciled with GAPS closures - Neutralized remaining org-voice leaks and research-harness provenance in resources; FROM SCRATCH wording reconciled with lesson-13 coexist rule - catalog.json churn reduced to a minimal +13/-1 diff Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(voice-agent-factory): review round 2 — webhook schema keys, gap-status consistency - GAPS webhook sketch: headers_schema -> request_headers; auth_connection / auth_resolved_params bullet (per WebhookToolApiSchemaConfig-Input) - REPORT closing paragraph now lists only remaining key-test checks; schema + pricing marked closed-in-GAPS / re-confirm at key-test - SOURCES server-tools line reworded to resolved-in-GAPS Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Boris <noreply@anthropic.com>
…skill (Rob-approved)
…grandamenium#593) agent-pty.ts hard-coded `--dangerously-skip-permissions` on every Claude agent spawn, which overrides settings.json entirely and disables ALL of Claude Code's permission gating — so the PermissionRequest hook (hook-permission-telegram) can never fire for tool use, regardless of the user's permission config. The Telegram approval gate was effectively dead for Claude agents. Add `AgentConfig.dangerously_skip_permissions` (default true for back-compat — agents have historically run unattended). Setting it to false omits the flag at spawn so Claude Code's permission system engages and the approval hook actually gates tool use. Only the literal boolean false disables the skip; a non-boolean value warns and falls back to skip-on, so a typo can't silently leave an agent ungated. Hermes already omits the flag, so only the claude-code spawn path is affected. This is the spawn-time half of the permission-gate work; the hook-hardening change is the matching correctness fix for the gate this toggle engages. Tests: flag present by default / when true / when undefined; absent when false; non-boolean warns and keeps the flag. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ggle (Rob-approved)
When an agent's MCP server hangs during init (e.g. connecting to a dead endpoint), the PTY stays alive but never bootstraps — the daemon reports "running" while the session is effectively dead. This adds a configurable boot watchdog (default 300s) that kills the hung PTY, arms .force-fresh to skip --continue, and restarts clean. Root cause: googli hit 71h session expiry, --continue restart spawned into an Obsidian MCP connecting to 127.0.0.1:27124 (not running), blocking init indefinitely. Diagnosed by dexter, fix by coder. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…he brief-alive corpse (grandamenium#620 follow-up) The soak corpse-E2E found the immediate pid-check missed the real gen-B shape: a node-pty corpse holds a briefly-alive WRAPPER pid that passes the instant probe, then dies as the exec fails inside — so it slipped to fast-checker C (abort only) or crash-recovery, never spawn-failed+alert. Bootstrap-completion is now the semantic line: - SETTLE: after spawn, poll pid-aliveness across a bounded INJECTABLE window (AgentProcess.spawnSettleMs, default 500ms) so a brief-alive wrapper that dies is caught and routed to the budget, not declared Running. - UNIFIED BUDGET: ANY pre-bootstrap exit (settle-caught or later via handleExit) routes to onPreBootstrapExit → retry up to MAX_SPAWN_ATTEMPTS → SPAWN-FAILED + fleet alert + STOP. spawnAttempts persists across retries. - handleExit is one branch: everBootstrapped ? crash-recovery : pre-bootstrap budget. markBootstrapped() (from the fast-checker on real bootstrap) latches everBootstrapped and resets the budget. This REPLACES (not double-bounds) the old path where a pre-boot corpse crash-looped up to max_crashes_per_day (~10, silent, as 'crashed'); crash-recovery now serves post-bootstrap only. - fast-checker aborts on terminal spawn-failed (not a momentary mid-retry death), and a timeout with a dead process no longer 'proceeds anyway'.
Re-integrates boot_timeout_seconds watchdog (5685bdc) into the restructured spawn-verify flow from upstream. Same behavior: kills and force-fresh restarts agents that don't bootstrap within timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…randamenium#339) A crash-recovery restart re-spawns the PTY but nothing verifies the --continue session actually RESUMES: the fast-checker runs waitForBootstrap once per discovery, not per crash-restart (empirically anchored on cdd8fc6), so a restart into a non-responsive / onboarding-wedged session is undetected (observed live: a kill -9'd agent came back 'Running' but sat idle on an onboarding TUI, never heartbeating, for 26min). Mechanism B (wedge-detection): after each crash-recovery restart, require a heartbeat STRICTLY NEWER than the restart within wedgeDetectMs (5min fixed). A healthy resume writes one within ~1-2min via its boot/resume routine; a wedged session never does. Absent => arm .force-fresh + exactly ONE re-restart => still wedged => ONE operator alert (restart-wedged class, 15-min dedup). last_task} in the agent state dir, piggybacking the SessionEnd crash-alert hook's classification (overwritten each end, mirrors crashes.log) so the dashboard can distinguish planned restarts from crashes without log-parsing. Part-1 (.session-refresh marker before stop) is already satisfied in the base (marker write precedes stop() in sessionRefresh; existing regression test covers it). Codex session-cap rollover routes through the SAME generic sessionRefresh (max_session timer, all runtimes) — no codex-specific path. Also env-pins the PRE-EXISTING notifyAgents transport tests (cdd8fc6, not this patch): they asserted only the CTX_FRAMEWORK_ROOT-unset fallback branch and broke when CTX_FRAMEWORK_ROOT was set. Now each case pins the var and BOTH branches (process.execPath+dist/cli.js when set; cortextos-on-PATH fallback) are asserted explicitly, so the suite is env-independent. Tests: wedge-detection resume-vs-wedge acceptance (force-fresh re-restart cap=1 -> ONE alert), exit-reason.json schema/overwrite/best-effort, notifyAgents both transport branches. tsc 0; targeted daemon+hooks green 43/43 BOTH env-scrubbed AND with CTX_FRAMEWORK_ROOT set. NOTE: N=5min and the restart-wedged alert use cdd8fc6-native primitives, not the grandamenium#621 staleness module / grandamenium#620 spawn-failure-alerter (different PR bases) — unification points called out in the delivery notes.
…-scope Original B:F-09 wanted MCP-flap detection in handleExit (mirror the image-poison crash detector). Repro on claude v2.1.170 FALSIFIED that premise: a broken .mcp.json (malformed JSON AND valid-but-unreachable-server) no longer crashes — claude boots DEGRADED with a non-fatal warning and runs WITHOUT its MCP tools. There is no crash, so handleExit can't fire; that mechanism is obsolete on current claude (see MCP-FLAP-REPRO-FINDING.md). Re-scoped to surface the degraded state (an agent silently running without its MCP tools is the same silent-failure class, just non-fatal): - checkMcpSetupWarningOnBoot(): after bootstrap, scan the boot stdout tail (ANSI stripped) for the exact captured warning structure — /setup issues:.*MCP.*\/doctor/ — anchored on the setup-issues + MCP + /doctor triple so normal output that merely mentions mcp can't false-positive (no guessed signatures; this is the real captured line). - ONE operator alert naming .mcp.json + a /doctor hint. Surface-only: NO restart/backoff (the agent is alive). - Once-per-incident dedup via a latch CLEARED on a warning-free boot. The post-bootstrap placement gives the clean latch-clear the handleExit path structurally couldn't — so no flat-4h fallback is needed here. - Wired from the fast-checker right after 'Bootstrap complete'. Tests (4, green dual-env scrubbed + CTX-set): degraded → ONE alert naming .mcp.json; clean → zero; false-positive guard (output mentioning mcp + /doctor but not the triple → no alert); once-per-incident (re-degraded no re-alert, a clean boot re-arms). fast-checker mock gains the method.
…Area 4.3, B:F-04) resetCrashCountIfNewDay ran ONLY from the crash path (handleExit), so a HALTED agent — which stops crashing once the daemon stops restarting it — never got its budget reset on a new day. In a long-running daemon across midnight, an agent halted yesterday stayed halted indefinitely until a manual .crash_count_today wipe. Fix: - resetCrashBudgetIfNewDay(): non-incrementing budget reset for start() + the daily check (the crash-path resetCrashCountIfNewDay keeps its +1 semantics). Only WRITES when clearing a genuine stale date (same-day = load, absent = no-op). - start() clears a stale budget up front → first start of a new day gets a fresh budget with no manual wipe. - Top-of-hour daily timer (aligned to :00 so all agents tick together at the day-roll; crossed-UTC-day guard; UTC toISOString day, no TZ logic). On the roll a crash-budget HALTED agent is auto-unhalted + restarted. - G2: agents carrying operator-intent markers (.user-disable/.user-stop) are NEVER auto-restarted on the day-roll — they stay down. - G1: auto-unhalt notices BATCH across agents — a module-level collector + 10s debounce emits ONE telegram naming all unhalted agents + count, never per-agent (same shape as the spawn-failure alerter). Tests (4, green dual-env scrubbed + CTX-set): start-path stale-date → today:0; daily-check new-day → reset+unhalt+restart; G2 user-disabled halted agent stays DOWN on day-roll; G1 two halted agents → ONE batched notice. Existing daemon-shutdown test drove the only-write-on-stale-clear refinement.
…+ first-heartbeat clear (grandamenium#445) Replay of PR grandamenium#445 (asachs01) onto main 2ae6356. Net effect of the PR's 4 commits (993dafe/6bd4f92/7f32046/3faca36): write .session-refresh marker so SessionEnd hook does not classify --continue rollovers as crashes; no-unlink marker handling + first-heartbeat clear; clearEndMarkers timing-race hardening. Applied 3-way clean onto current main (no conflicts). Co-Authored-By: asachs01 <asachs01@users.noreply.github.com>
grandamenium#609 grandamenium#202) Orphaned lifecycle markers made the crash-alert hook misread a genuine crash as an intentional stop, silently masking dead agents (overnight: 14.5h). Four fixes, preserving the deliberate firing#2 grace window: (c) enable consumes stale markers: cortextos enable now deletes a prior disable/stop/restart's .user-disable/.user-stop/.restart-planned/etc before the daemon start, so a re-enabled agent's first crash is seen. (d) daemon boot sweep: discoverAndStart reaps lifecycle markers older than the 300s hook TTL across every agent (in-flight ones < TTL are left to the heartbeat grace path; .daemon-crashed excluded). Each clear logs agent/marker/age/content — the observability for this class. This is what self-heals an orphaned-marker agent on daemon restart. (a) the orphaned .restart-planned from a crash-before-heartbeat (or a re-armed restart loop refreshing its mtime past the grace window) is reaped by (d) at the next boot; misclassification is bounded by the existing 300s TTL. (b) hard-restart now actually kills a wedged PTY: the wrapper forwards the kill signal (was silently dropped), and stop() escalates to SIGKILL on the whole PROCESS GROUP if the child survives SIGTERM — reaping descendants a bare kill would orphan (orphaned PTY children are the suspected OS resource-exhaustion mechanism). Single canonical marker list exported from heartbeat.ts. Tests cover the disable->enable cycle, the stale-vs-in-flight sweep + its log line, the .daemon-crashed exclusion, and the process-group SIGKILL + fallback.
…ndamenium#619/grandamenium#202 (b) completion) The group SIGKILL (process.kill(-pid)) only reaps the leader's process group. A child that put itself in its OWN process group (job control, setpgid, detached helper) survives it and orphans to pid 1 — the exact own-pgroup case the teardown audit found uncovered. Snapshot the leader's descendant tree by ppid BEFORE the graceful kill (while still attached), then on teardown SIGKILL each survivor by pid in addition to the group signal, so no descendant orphans regardless of its process group.
6.1 (A:F-08): poll getUpdates with a 25s long-poll window instead of timeout=1 short-polling, cutting idle Telegram API traffic ~25x with no added message latency. The long-poll window must sit below the getUpdates HTTP abort, so TelegramAPI.getUpdates now scales its AbortSignal timeout to (timeout+5)s (>=15s) via a new optional post() timeout param; other methods keep the 15s default. 6.3 (C#1): split the previously-conflated poll-error path via a pure planPollError() helper. 409 Conflict self-dies (unchanged); 429 honours Telegram's 'retry after N' value; network/timeout errors get exponential backoff (1s..60s cap) with a circuit-breaker log at 5 consecutive failures. Per-cycle jitter de-syncs the fleet on shared outages. The network streak resets on any cycle that reaches Telegram (success or 429).
Agents are supposed to restore crons from config.json on session start, but compliance is unreliable (agents sometimes skip step 6 of the 13-step boot sequence). This adds a framework-level safety net: After the agent finishes its startup turn (detected via last_idle.flag), the daemon reads the crons array from config.json and injects a verification prompt asking the agent to check CronList and restore any missing recurring crons. Safe for both fresh and --continue restarts: waits for idle before injecting, so it never interrupts a mid-conversation agent. Bails out if the agent stops or restarts during the wait window. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Issue 1: verifyCronsAfterIdle had a timeout fallthrough bug where the while-loop exit via timeout (no idle flag detected) fell through into the injection block, causing a prompt to be injected into an agent mid-work. Fix adds a `foundIdle` boolean — injection is skipped unless the loop explicitly broke on a newer idle timestamp. Issue 2: Add happy-path test for verifyCronsAfterIdle covering the core flow: existsSync returns true with a boot timestamp, then a newer timestamp on the first poll, triggering injection with expected cron names. Uses vi.useFakeTimers() to advance the 15s poll instantly. Also adds the three guard-clause tests (no-crons, once-only, recurring) and wires mockInjectMessage for assertion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add stdin/file body input paths to `cortextos bus send-message` so a message body is never interpolated into a shell-expanded argument. A body passed positionally is subject to the caller's shell expansion (backticks, $()) BEFORE the CLI runs — the root of the 2026-06-02 command-injection incident where a bus message body command-substituted and force-moved a branch. The send layer itself was already safe (quoted array -> exec node); the only effective fix is an input path that bypasses argument-level shell expansion, which stdin/file provide without mangling legitimate code-in-body content (body-content stripping would corrupt it and cannot prevent caller-side expansion). - resolveMessageBody() helper: stdin > --body-file > positional <text> - CLI: --body-stdin, --body-file <path>; <text> now optional - bus/send-message.sh: flag passthrough for the safe forms - tests: 6 cases (precedence, literal backtick/$() preservation, empty stdin, error) - docs in --help + wrapper header mandating single-quote-or-stdin for code bodies Closes the command-injection-via-message-body class. Verified: typecheck clean, 14/14 message tests pass, smoke test confirms $()/backticks stored literally with no execution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fleet-health watchdog used a FLAT 5h staleness threshold, so an agent dead 3.5h read healthy (paul, 01:00Z) — a flat threshold can't tell a 5-min-cadence agent from a 4h-cadence one. Derive the threshold from the agent's own heartbeat loop_interval: threshold = max( min(3x interval, 6h), 1.5x interval, 15m ) The 1.5x floor is the cap-bite guard: without it a cadence at/above the 6h cap (e.g. a 6h heartbeat cron) would flag at its own normal beat. Missing or unparseable loop_interval falls back to a conservative 2h, logged once per process so a misconfigured agent surfaces without spamming every cycle. Wires computeStaleThresholdMs into collectMetrics (src/bus/metrics.ts), replacing the flat 5h. Existing metrics-staleness tests still pass (no loop_interval => 2h fallback: fresh healthy, 6h-old stale). Tests: trio (3x-band / cap-bite guard / 15m floor + 2h fallback) + paul-regression (1h cadence, dead 3.5h => stale; the old flat 5h read it healthy).
Spawn-verify retry loop intercepts pre-bootstrap exits — tests that simulate post-bootstrap crashes need markBootstrapped() so exits route through crash-recovery instead of spawn-retry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add /portal/[clientId]/reports with 8 KPI cards, spend-by-platform, zero-conversion campaigns, CPA vs target, daily performance table, and date-range selector (7/14/30/60/90 days) - Add entity_type='campaign' filter to all daily_metrics queries in bq-clients.ts and portal-questions.ts (prevents ~4x spend inflation) - Add 'client' role to RBAC (types, abilities, auth JWT, middleware) - Add client_id column to users table for binding clients to their data - Client-role middleware routing: restrict to own portal, redirect on admin route access - Add /api/client-users endpoint for admin to create client logins - Update portal layout with nav links (reports, creatives, billing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "Portal" column to clients list with View Portal link - Add Client Portal section on client detail page with report link - Both link to /portal/[clientId]/reports (the new reports page) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TypeScript script pulls CampaignPerformanceReport via Bing Ads Reporting API v13, writes to click-to-acquire.analytics.daily_metrics with platform='bing', entity_type='campaign'. Covers both sub-accounts (Click To Acquire + OC Repipes). Includes shell wrapper and launchd plist for daily 06:00 ET execution. Requires: MSADS_CLIENT_ID, MSADS_CLIENT_SECRET, MSADS_REFRESH_TOKEN in orgs/click-to-acquire/.env (OAuth refresh token flow). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Custom cookies config overrode Auth.js defaults without secure:true, causing __Secure- prefixed cookies to be rejected by browsers on HTTPS (Vercel). Also fixed submitToAuthJs using stale csrfTokenRef instead of the freshly-fetched submitToken variable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
clicktoacquire
pushed a commit
that referenced
this pull request
Jul 2, 2026
fix(fast-checker): remove stdout.log size check from isAgentActive() — fixes permanent typing indicator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test plan