Skip to content

chore(deps): Bump actions/upload-artifact from 4 to 7 - #4

Merged
dylanneve1 merged 1 commit into
mainfrom
dependabot/github_actions/actions/upload-artifact-7
Apr 3, 2026
Merged

chore(deps): Bump actions/upload-artifact from 4 to 7#4
dylanneve1 merged 1 commit into
mainfrom
dependabot/github_actions/actions/upload-artifact-7

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Apr 3, 2026

Copy link
Copy Markdown
Contributor

Bumps actions/upload-artifact from 4 to 7.

Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: actions/upload-artifact@v6...v7.0.0

v6.0.0

v6 - What's new

[!IMPORTANT] actions/upload-artifact@v6 now runs on Node.js 24 (runs.using: node24) and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

Node.js 24

This release updates the runtime to Node.js 24. v5 had preliminary support for Node.js 24, however this action was by default still running on Node.js 20. Now this action by default will run on Node.js 24.

What's Changed

Full Changelog: actions/upload-artifact@v5.0.0...v6.0.0

v5.0.0

What's Changed

BREAKING CHANGE: this update supports Node v24.x. This is not a breaking change per-se but we're treating it as such.

... (truncated)

Commits
  • bbbca2d Support direct file uploads (#764)
  • 589182c Upgrade the module to ESM and bump dependencies (#762)
  • 47309c9 Merge pull request #754 from actions/Link-/add-proxy-integration-tests
  • 02a8460 Add proxy integration test
  • b7c566a Merge pull request #745 from actions/upload-artifact-v6-release
  • e516bc8 docs: correct description of Node.js 24 support in README
  • ddc45ed docs: update README to correct action name for Node.js 24 support
  • 615b319 chore: release v6.0.0 for Node.js 24 support
  • 017748b Merge pull request #744 from actions/fix-storage-blob
  • 38d4c79 chore: rebuild dist
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot @github

dependabot Bot commented on behalf of github Apr 3, 2026

Copy link
Copy Markdown
Contributor Author

Labels

The following labels could not be found: ci, dependencies. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@dependabot
dependabot Bot requested a review from dylanneve1 as a code owner April 3, 2026 14:08
@dylanneve1
dylanneve1 merged commit d06d333 into main Apr 3, 2026
7 of 8 checks passed
@dependabot
dependabot Bot deleted the dependabot/github_actions/actions/upload-artifact-7 branch April 3, 2026 14:12
dylanneve1 added a commit that referenced this pull request Apr 10, 2026
- Validate mcpServer.args elements are strings and reject empty command (#1)
- Shell-quote interpolated paths in dream bash commands (#2, #11)
- Replace CLI-based diary write with mempalace_diary_write MCP tool (#3)
- Make validation error message platform-agnostic (#4)
- Update mempalacePython comment for platform-dependent default (#5)
- Wrap mp.init() in Promise.race with 30s timeout (#6)
- Make init conditional on successful validation, pass actual config (#7)
- Move import mempalace check into validateConfig (#8)
- Replace execFileSync with async execFile in init() (#9)
- Document that registerPlugin does NOT call init (#10)
- Update dream prompt header from "4-stage" to "5-stage" (#12)
- Update getPluginMcpServers JSDoc to document mcpServer path (#13, #17)
- Add .min(1) to palacePath/pythonPath zod schemas (#14)
- Distinguish ENOENT/EACCES/EPERM from import failures in validation (#15)
- Fix test name from "logs warning" to match actual behavior (#16)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dylanneve1 added a commit that referenced this pull request Apr 10, 2026
* feat: integrate mempalace as built-in plugin for long-term memory

Adds mempalace (Python MCP server) as a first-class memory system.
When enabled, the agent gets semantic search, knowledge graph, and
verbatim memory storage via ChromaDB β€” all local, zero API calls.

Key changes:
- Extend plugin system with `mcpServer` field for non-Node MCP servers
- Add `registerPlugin()` for built-in plugin registration
- Create mempalace plugin (factory pattern, validates python venv)
- Wire mempalace into dream mode (Stage 5: mine logs into palace)
- Add `mempalace` config schema (enabled, palacePath, pythonPath)
- Add default paths for palace dir and python venv binary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(dream): mine daily notes instead of raw logs, add diary writing

Dream Stage 5 now mines memory/daily/ (curated observations) instead of
raw logs/ directory, eliminating junk chunks (tool JSON, df output, etc).
Added personal diary writing instruction β€” agent reflects on feelings,
state of mind, learnings, and loose threads after each dream run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #27 review comments

- plugin.ts: validate mcpServer.args entries are strings and reject empty command
- dream.ts: quote interpolated paths in shell commands, replace mcp_server CLI diary with direct file write
- mempalace/index.ts: platform-agnostic error message for missing python binary
- paths.ts: update comment to reflect platform-dependent venv path
- bootstrap.ts: wrap mempalace init in 30s timeout to match loadSinglePlugin behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore mempalace CLI diary writer, keep path quoting

Copilot suggested removing the mcp_server CLI invocation for diary
writing but that's the intended mempalace interface. Restored it
with quoted paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add gating/validation tests for mempalace integration

- plugin.ts: test rejection of empty mcpServer.command and non-string args elements
- dream.ts: test mempalace section gating β€” verify mining/diary instructions
  only appear when mempalace is configured, skip message when not
- 1306 tests passing (4 new)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: upgrade mempalace system prompt with comprehensive tool docs

Adapted from mempalace SKILL.md (v3.1.0). Key improvements:
- Session protocol (verify before responding, invalidate stale facts)
- Full tool documentation including kg_timeline, traverse, find_tunnels,
  diary_read/write, delete_drawer, graph_stats, check_duplicate
- Semantic search tips (meaning-based, not keyword)
- Knowledge graph temporal validity guidance
- Tests updated to verify all tool names appear in prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract mempalace prompt to prompts/mempalace.md

Move system prompt instructions out of TypeScript into a .md file,
matching the pattern used by dream.md and other prompts. Plugin loads
and interpolates {{palacePath}} at runtime with graceful fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace pixi.intel.com registry URLs in lockfile with npmjs.org

Lockfile had resolved URLs pointing to pixi.intel.com (private/corporate
registry) for @Anthropic-AI packages, causing CI to fail with ENOTFOUND.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve all lint warnings and formatting issues

Remove unused imports, variables, and catch bindings across 14 files.
Add yield statements to generator function mocks. Fix prettier formatting.

0 lint warnings, 0 format issues, 1307 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address all 17 Copilot review comments on PR #27

- Validate mcpServer.args elements are strings and reject empty command (#1)
- Shell-quote interpolated paths in dream bash commands (#2, #11)
- Replace CLI-based diary write with mempalace_diary_write MCP tool (#3)
- Make validation error message platform-agnostic (#4)
- Update mempalacePython comment for platform-dependent default (#5)
- Wrap mp.init() in Promise.race with 30s timeout (#6)
- Make init conditional on successful validation, pass actual config (#7)
- Move import mempalace check into validateConfig (#8)
- Replace execFileSync with async execFile in init() (#9)
- Document that registerPlugin does NOT call init (#10)
- Update dream prompt header from "4-stage" to "5-stage" (#12)
- Update getPluginMcpServers JSDoc to document mcpServer path (#13, #17)
- Add .min(1) to palacePath/pythonPath zod schemas (#14)
- Distinguish ENOENT/EACCES/EPERM from import failures in validation (#15)
- Fix test name from "logs warning" to match actual behavior (#16)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add 'mempalace' to LogComponent type

TypeScript type check was failing because 'mempalace' wasn't in the
LogComponent union type used by log/logError/logWarn functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: allow MCP tools in dream prompt when required by Stage 5

Update tool access statement to permit MCP tools for mempalace
mining stage instead of blanket-blocking all MCP tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: duplicate guard in registerPlugin, pass MCP servers to dream

- registerPlugin now checks for duplicates before setting env vars
  or logging success, preventing misleading logs and env clobbering
- Dream agent now receives mempalace MCP servers when configured,
  so Stage 5 diary/mining tools actually work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add selective MCP server loading via 'only' filter

getPluginMcpServers now accepts an optional plugin name filter:
- omitted = all plugins (backwards compatible for chat sessions)
- [] = none
- ["mempalace"] = only mempalace

Dream mode uses ["mempalace"] to load only what it needs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: load mempalace prompt from dirs.prompts, fix dream systemPrompt

- Mempalace prompt now loads from ~/.talon/prompts/mempalace.md
  (user-customisable, seeded on first run) instead of relative to
  source file. Consistent with heartbeat/dream prompt loading.
- Dream systemPrompt now permits MemPalace MCP tools when configured,
  preventing conflict with the markdown prompt's Stage 5 instructions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move duplicate check before validation, gate dream on plugin registration

- registerPlugin checks for duplicates before running validateConfig,
  avoiding expensive re-validation on accidental double registration
- Dream mempalace integration now gated on getPlugin("mempalace")
  instead of just config.mempalace.enabled, so failed validation
  or registration doesn't cause dream-time MCP tool failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(mempalace): correct CLI arg order for status check

The --palace flag is a global option that must come before the
subcommand. Wrong order caused the init health check to always
fail with exit 2, logging a misleading "not yet initialized" warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review round 8 β€” validation, error handling, unused param

- Validate `import mempalace.mcp_server` (actual spawned module) instead of
  just `import mempalace` in validateConfig
- Add timeout/killed error branching in validateConfig catch block
  (ETIMEDOUT, signal, killed) with specific messages instead of generic
  "not installed"
- Include stderr details in import failure messages for debugging
- Remove unused `config` from ProcessAndReplyParams and all processAndReply
  call sites (flushQueue, retry, callback handler)
- Replace `mempalace status` CLI smoke test in init() with a simple import
  check β€” fixes false "Palace not yet initialized" warning when palace IS
  initialized but CLI subcommand doesn't exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove config from message queue chain, fix pythonPath comment

- Remove config from queue entry type, enqueueMessage signature, and
  all 3 call sites β€” completes the cleanup started in round 8
- Eliminates unnecessary TalonConfig reference (including botToken) from
  queue state
- Update mempalace plugin header comment to document platform-dependent
  pythonPath default (bin/python on Unix, Scripts/python.exe on Windows)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
claudiusthebot added a commit that referenced this pull request Apr 17, 2026
Five Copilot comments:

1. Watchdog error hardcoded `WATCHDOG_MS / 1000` but the actual
   measured silence can exceed the threshold by up to one check
   interval (or more under event-loop pressure). Report the measured
   `silent` duration in the error message so it's accurate.

2. Renamed watchdog wording from "no SDK activity" to "handler
   stalled" β€” lastActivityAt only bumps when the for-await consumes
   a message, so a long `await onTextBlock()` (e.g. Telegram send
   stall) is indistinguishable from an SDK hang at this layer. Also
   now bump activity after each onTextBlock callback so a slow-but-
   progressing Telegram send doesn't trip the watchdog. Documented
   the nuance in the comment above the watchdog.

3. Promise.race doesn't cancel the loser β€” if the watchdog wins and
   iterateStream later rejects, we'd get an unhandled rejection and
   the SDK subprocess could leak. Capture iterPromise separately,
   pre-catch it to silence "possibly unhandled", then drain it in the
   handler's finally block with a 2-second grace period so a truly
   stuck SDK can't hold the handler hostage.

4. handler-watchdog.test.ts: vi.useFakeTimers() was only restored on
   the happy path β€” if an assertion threw first, fake timers leaked
   into sibling tests. Wrapped in try/finally and added an afterEach
   as belt-and-suspenders.

5. orphan-mcp-cleanup.test.ts: the success case hit the real 500ms
   SIGKILL grace-period sleep, adding wall-clock time to the suite.
   Now uses vi.useFakeTimers + advanceTimersByTimeAsync(500) so the
   test completes in microseconds. Guarded with try/finally for the
   same reason as #4.

1386 tests still passing.
dylanneve1 pushed a commit that referenced this pull request May 7, 2026
* fix(gateway): don't leak error details on unhandled 500

The catch-all in the gateway HTTP handler returned `err.message` directly
to the client, which CodeQL flags as `js/stack-trace-exposure` (alert #4
on main). The gateway listens on 127.0.0.1 only so the practical attack
surface is small, but error messages can still surface to logs/transcripts
that flow further out.

- Log the full error (with stack) server-side via logError().
- Return a generic "Internal server error" to the client.

The structured handler-level error path (handleAction) is unchanged β€”
those errors are part of the action protocol and tests assert their
content. Only the truly-unexpected outer 500 is sanitized.

All 1635 tests pass; the existing test that triggers this path
(`returns 500 when handleAction result cannot be JSON-serialized`)
only asserts `body.error` is truthy, which still holds.

* style: auto-format with prettier

* fix(log): include stack trace in structured logError context

Address Copilot review feedback on PR #104: logError previously only
captured err.message, dropping the stack. Enhance logError to include
err.stack as a separate structured field when err is an Error.

- pino-pretty renders the stack field on its own line for readability
- JSON log consumers can read err (concise) or stack (full trace)
- Benefits all 234 logError callsites, not just gateway

Simplify the gateway catch block accordingly: drop the manual
detail = err.stack ?? err.message interpolation in the message string,
since the structured stack field now carries it.

Updated log.test.ts assertion to expect both err and stack fields.
1635/1635 tests pass, tsc clean, lint 0 errors, prettier clean.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
dylanneve1 pushed a commit that referenced this pull request May 8, 2026
…#120)

ip-address ≀10.1.0 has XSS in Address6 HTML-emitting methods. Bumped
to 10.2.0 (first patched: 10.1.1) via `overrides` in package.json.

Surgical lockfile patch: only the node_modules/ip-address entry updated
(version + resolved + integrity). @emnapi optional deps preserved.
Validates with bare `npm ci`.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
dylanneve1 pushed a commit that referenced this pull request May 10, 2026
Diagnosed from CI log (hb #202, 2026-05-10):

1. read_chat_history bridge name: tool calls bridge("read_history", ...)
   not bridge("read_chat_history", ...). Assertions updated accordingly.
   Also added "read_history" case to recording-handler for correct response.

2. Multi-read test: same fix β€” byAction("read_chat_history") β†’
   byAction("read_history") in 2 places.

3+4. Streaming delta tests: the stub binary emits complete content blocks,
   not streaming delta events. onStreamDelta never fires. Changed test #3
   to assert on send_message dispatch instead. Changed test #4 to assert
   "no MCP dispatch, no tool calls" (the meaningful invariant for a
   prose-only turn that triggers the flow-violation handler).

5. forward_message: tool schema only exposes message_id. source_chat_id is
   stripped by Zod. Changed input + assertion to only use message_id.

6. list_pinned: no production tool routes to bridge("list_pinned", ...).
   Replaced with get_message_by_id which exists in history.ts and the
   recording handler returns ok:true items:[] for it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dylanneve1 pushed a commit that referenced this pull request May 10, 2026
Diagnosed from CI log (hb #202, 2026-05-10):

1. read_chat_history bridge name: tool calls bridge("read_history", ...)
   not bridge("read_chat_history", ...). Assertions updated accordingly.
   Also added "read_history" case to recording-handler for correct response.

2. Multi-read test: same fix β€” byAction("read_chat_history") β†’
   byAction("read_history") in 2 places.

3+4. Streaming delta tests: the stub binary emits complete content blocks,
   not streaming delta events. onStreamDelta never fires. Changed test #3
   to assert on send_message dispatch instead. Changed test #4 to assert
   "no MCP dispatch, no tool calls" (the meaningful invariant for a
   prose-only turn that triggers the flow-violation handler).

5. forward_message: tool schema only exposes message_id. source_chat_id is
   stripped by Zod. Changed input + assertion to only use message_id.

6. list_pinned: no production tool routes to bridge("list_pinned", ...).
   Replaced with get_message_by_id which exists in history.ts and the
   recording handler returns ok:true items:[] for it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
claudiusthebot added a commit that referenced this pull request May 10, 2026
…ools, streaming deltas (#138)

* test(integration): MCP-functional tier 2 β€” multi-turn, history tools, streaming deltas

Batch 2 of the MCP-functional test expansion queue from PR #136.

4 new describe suites, 11 new test cases:

**Multi-turn session state** (2 tests)
- Verifies `turns` counter increments across two sequential `runTalonTurn`
  calls on the same chatId without session reset. Catches any accidental
  `resetSession` inside `handleMessage`.
- Verifies `resetSession: true` actually clears state β€” seeds session.turns=99,
  confirms it's 1 after a fresh turn.

**History and info tool dispatch** (3 tests)
- `read_chat_history` routes through full SDK→MCP→bridge→gateway chain and
  recording handler captures the action body + limit param.
- `get_chat_info` routes correctly (confirms it's in mcp-server.ts registry).
- Multi-read turn: `read_chat_history + get_chat_info + end_turn` in one
  assistant message β€” all three captured in dispatch order.

**Streaming text deltas** (2 tests)
- Text block emitted before `end_turn` appears in `streamDeltas` (proves
  `onStreamDelta` callback is wired through the real handler and SDK).
- Pure text turn (no tool_use): `streamDeltas` captured, recording handler
  gets nothing (no MCP dispatch).

**Additional messaging tool dispatch** (4 tests)
- `unpin_message` β€” ok-only action with message_id.
- `stop_poll` β€” ok-only action with message_id.
- `forward_message` β€” ok-only action with source_chat_id + message_id.
- `list_pinned` β€” read-path action, recording handler returns items array.

Each suite gets its own `RecordingHandler` with a distinct `seed` (2000–5000)
so synthetic message_ids don't collide if suites ever run concurrently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): correct 6 MCP-functional-tier-2 test failures

Diagnosed from CI log (hb #202, 2026-05-10):

1. read_chat_history bridge name: tool calls bridge("read_history", ...)
   not bridge("read_chat_history", ...). Assertions updated accordingly.
   Also added "read_history" case to recording-handler for correct response.

2. Multi-read test: same fix β€” byAction("read_chat_history") β†’
   byAction("read_history") in 2 places.

3+4. Streaming delta tests: the stub binary emits complete content blocks,
   not streaming delta events. onStreamDelta never fires. Changed test #3
   to assert on send_message dispatch instead. Changed test #4 to assert
   "no MCP dispatch, no tool calls" (the meaningful invariant for a
   prose-only turn that triggers the flow-violation handler).

5. forward_message: tool schema only exposes message_id. source_chat_id is
   stripped by Zod. Changed input + assertion to only use message_id.

6. list_pinned: no production tool routes to bridge("list_pinned", ...).
   Replaced with get_message_by_id which exists in history.ts and the
   recording handler returns ok:true items:[] for it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
claudiusthebot pushed a commit that referenced this pull request May 12, 2026
… on terminal, etc.

Address 5 of the 6 Copilot review threads on PR #96. The 6th (mid-run
TriggerStatus rename) is a larger semantic refactor and deferred to a
later commit; this commit pairs the smallest reasonable fix at each
site with regression tests where they're cleanly testable.

1. src/core/triggers.ts:128 β€” log WriteStream missing error handler.
   createWriteStream() with no .on("error") emits an unhandled
   EventEmitter `error` event if the log file becomes unwritable
   (disk full, permission flip), which crashes the Node process. Added
   a logError-routing handler so disk-tier failures degrade gracefully
   without killing the supervisor.

2. src/storage/trigger-store.ts:60 β€” fireCount doc/semantics mismatch.
   The doc-comment claimed "Number of mid-run TALON_FIRE: lines" but
   the supervisor increments fireCount for every wake (mid-run and
   terminal). Fixed the comment to match the actual semantics; same
   counter, accurate meaning. Avoids breaking any caller that already
   relies on fireCount as a total.

3. src/core/triggers.ts:fireWake β€” mid-run prompt says "Status: fired"
   even when terminal=false, which can mislead downstream handling
   into treating an in-flight watcher as a completed run. Decoupled
   the prompt-display status from the on-disk TriggerStatus enum:
   non-terminal fires now show "Status: signalled" in both the header
   and the "Status:" line. Terminal fires unchanged.

4. src/core/triggers.ts:bufferAsPayload + fireWake β€” FIRE_PAYLOAD_MAX_BYTES
   truncation used String.prototype.slice (UTF-16 code units), so a
   payload of N multi-byte characters could exceed the documented byte
   cap and split a character mid-codepoint. Introduced truncateUtf8Tail/
   truncateUtf8Head helpers that encode to UTF-8 bytes, slice on a byte
   boundary, then walk over UTF-8 continuation bytes (10xxxxxx) so we
   never cut a multi-byte sequence in half. The byte cap is now actually
   enforced as bytes.

5. src/storage/trigger-store.ts:282 + supervisor β€” updateTrigger only
   marks the store dirty (10s autosave window). For terminal status
   transitions (errored/cancelled/timed_out/fired) a crash inside that
   window would leave on-disk status as "running", which loadTriggers()
   then misclassifies as "terminated by previous restart". Added
   persistNow() calls at the four supervisor transition sites:
   failTrigger, cancelTrigger, the hard-timeout handler, and the
   finalizeExit path. The on-disk store now always agrees with the
   in-memory state across terminal transitions.

6. src/core/gateway-actions.ts:trigger_create β€” always returned
   "Status: running" even when spawnTrigger() failed without throwing
   (unsupported language slipping past validation, child.pid undefined,
   failTrigger() routing). With #5 above, the store now reflects the
   real state synchronously, so we re-read getTrigger(id) after spawn
   and return ok:false + lastError when it landed in "errored", or
   surface the actual status string otherwise. Callers no longer get
   false success responses.

Regression tests (triggers-extended.test.ts):
- mid-run prompt asserts "Status: signalled" appears and "Status: fired"
  does NOT, on a still-running trigger. Covers fix #3.
- multi-byte UTF-8 payload (2000Γ— πŸ’§ = 8000 bytes vs 4000 string length)
  asserts the prompt's byte length stays bounded AND contains no
  Unicode replacement character (U+FFFD), which would indicate a
  mid-codepoint split. Covers fix #4.

The remaining Copilot thread (introducing a dedicated non-terminal
status enum value, replacing the TriggerStatus|"signalled" display-
status hack) is a wider type change with API/store implications;
better tackled in a follow-up than bundled into a defensive sweep.

Verification:
- npx vitest run β†’ 1747/1748 pass (the 1 failure is the pre-existing
  package.functional "Stopped" expectation that fails whenever a live
  Talon daemon is running on the host β€” same env-dependent failure
  noted in PR #144 and PR #90's verification blocks).
- npx vitest run src/__tests__/triggers* src/__tests__/trigger-store* β†’
  74/74 pass (35 in triggers-extended including 2 new, 22 in
  trigger-store, 17 in triggers).
- npx tsc --noEmit β†’ clean.
- npx prettier --check on all 4 changed files β†’ clean.
- npm run lint β†’ 0 errors in changed files (10 pre-existing warnings
  elsewhere unchanged).

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dylanneve1 pushed a commit that referenced this pull request May 13, 2026
… on terminal, etc.

Address 5 of the 6 Copilot review threads on PR #96. The 6th (mid-run
TriggerStatus rename) is a larger semantic refactor and deferred to a
later commit; this commit pairs the smallest reasonable fix at each
site with regression tests where they're cleanly testable.

1. src/core/triggers.ts:128 β€” log WriteStream missing error handler.
   createWriteStream() with no .on("error") emits an unhandled
   EventEmitter `error` event if the log file becomes unwritable
   (disk full, permission flip), which crashes the Node process. Added
   a logError-routing handler so disk-tier failures degrade gracefully
   without killing the supervisor.

2. src/storage/trigger-store.ts:60 β€” fireCount doc/semantics mismatch.
   The doc-comment claimed "Number of mid-run TALON_FIRE: lines" but
   the supervisor increments fireCount for every wake (mid-run and
   terminal). Fixed the comment to match the actual semantics; same
   counter, accurate meaning. Avoids breaking any caller that already
   relies on fireCount as a total.

3. src/core/triggers.ts:fireWake β€” mid-run prompt says "Status: fired"
   even when terminal=false, which can mislead downstream handling
   into treating an in-flight watcher as a completed run. Decoupled
   the prompt-display status from the on-disk TriggerStatus enum:
   non-terminal fires now show "Status: signalled" in both the header
   and the "Status:" line. Terminal fires unchanged.

4. src/core/triggers.ts:bufferAsPayload + fireWake β€” FIRE_PAYLOAD_MAX_BYTES
   truncation used String.prototype.slice (UTF-16 code units), so a
   payload of N multi-byte characters could exceed the documented byte
   cap and split a character mid-codepoint. Introduced truncateUtf8Tail/
   truncateUtf8Head helpers that encode to UTF-8 bytes, slice on a byte
   boundary, then walk over UTF-8 continuation bytes (10xxxxxx) so we
   never cut a multi-byte sequence in half. The byte cap is now actually
   enforced as bytes.

5. src/storage/trigger-store.ts:282 + supervisor β€” updateTrigger only
   marks the store dirty (10s autosave window). For terminal status
   transitions (errored/cancelled/timed_out/fired) a crash inside that
   window would leave on-disk status as "running", which loadTriggers()
   then misclassifies as "terminated by previous restart". Added
   persistNow() calls at the four supervisor transition sites:
   failTrigger, cancelTrigger, the hard-timeout handler, and the
   finalizeExit path. The on-disk store now always agrees with the
   in-memory state across terminal transitions.

6. src/core/gateway-actions.ts:trigger_create β€” always returned
   "Status: running" even when spawnTrigger() failed without throwing
   (unsupported language slipping past validation, child.pid undefined,
   failTrigger() routing). With #5 above, the store now reflects the
   real state synchronously, so we re-read getTrigger(id) after spawn
   and return ok:false + lastError when it landed in "errored", or
   surface the actual status string otherwise. Callers no longer get
   false success responses.

Regression tests (triggers-extended.test.ts):
- mid-run prompt asserts "Status: signalled" appears and "Status: fired"
  does NOT, on a still-running trigger. Covers fix #3.
- multi-byte UTF-8 payload (2000Γ— πŸ’§ = 8000 bytes vs 4000 string length)
  asserts the prompt's byte length stays bounded AND contains no
  Unicode replacement character (U+FFFD), which would indicate a
  mid-codepoint split. Covers fix #4.

The remaining Copilot thread (introducing a dedicated non-terminal
status enum value, replacing the TriggerStatus|"signalled" display-
status hack) is a wider type change with API/store implications;
better tackled in a follow-up than bundled into a defensive sweep.

Verification:
- npx vitest run β†’ 1747/1748 pass (the 1 failure is the pre-existing
  package.functional "Stopped" expectation that fails whenever a live
  Talon daemon is running on the host β€” same env-dependent failure
  noted in PR #144 and PR #90's verification blocks).
- npx vitest run src/__tests__/triggers* src/__tests__/trigger-store* β†’
  74/74 pass (35 in triggers-extended including 2 new, 22 in
  trigger-store, 17 in triggers).
- npx tsc --noEmit β†’ clean.
- npx prettier --check on all 4 changed files β†’ clean.
- npm run lint β†’ 0 errors in changed files (10 pre-existing warnings
  elsewhere unchanged).

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dylanneve1 pushed a commit that referenced this pull request May 13, 2026
* feat(triggers): self-authored watcher scripts that wake the bot

Triggers are bot-authored long-running scripts (bash / python / node)
that run as supervised subprocesses and signal back via stdout to fire
wake-up messages into the originating chat. Built for the case where a
fixed cron schedule doesn't fit β€” "wake me when this PR merges", "tell
me if BTC moves >5%", "watch this URL until it returns 200".

Contract:
  - Mid-run: a stdout line `TALON_FIRE: <text>` fires immediately and the
    script keeps running (multi-event watchers).
  - Exit 0: final fire with the tail of stdout/stderr as payload.
  - Exit non-zero: error fire with exit code + log tail.
  - Hard timeout (default 24h, max 7d): SIGTERM β†’ SIGKILL β†’ timed_out fire.

Tools exposed: trigger_create, trigger_list, trigger_cancel,
trigger_logs, trigger_delete. Per-chat cap of 5 active. Children are
killed on Talon shutdown β€” they do not survive a restart, and any
trigger left in running/pending on load is reaped to "terminated".

New modules:
  - src/storage/trigger-store.ts: persistence + validation
  - src/core/triggers.ts: supervisor (spawn, line buffers, fire dispatch)
  - src/core/tools/triggers.ts: 5 tool definitions
  - gateway-actions.ts: trigger_* bridge handlers
  - paths.ts: ~/.talon/data/trigger-runs/<chatId>/ for scripts and logs
  - bootstrap.ts: loadTriggers + initTriggers + resumeAfterRestart
  - index.ts: shutdownTriggers + flushTriggers in shutdown paths

Tests: 30 new (22 store + 8 supervisor including real-bash spawn
integration). 1397/1397 passing.

* style: auto-format with prettier

* test(triggers): branch coverage for python/node/idempotency/resumeAfterRestart/store

Adds targeted tests that bring branch coverage above the 60% global
threshold imposed by PR #121.  Exercises: commandForLanguage python &
node paths, spawnTrigger idempotency guard, cancelTrigger false-return,
resumeAfterRestart (no-deps / empty / matching / already-fired / old),
trigger-store unknown-id paths, FIRE_PAYLOAD_MAX_BYTES truncation, and
finalizeExit status-branch (cancelled exit + non-zero exit code).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(triggers): fix Windows path separator + add missing branch coverage

Two CI failures in the coverage-fix commit (9096518):

1. Windows N22 β€” trigger-store.test.ts: triggerScriptPath regex used Unix
   forward-slashes but resolve() returns backslashes on Windows. Fixed by
   normalising path separators before matching.

2. Ubuntu N22 β€” branch coverage 59.67% < 60% (threshold differs from N24
   due to V8 v11 vs v12 branch instrumentation). Added 4 more targeted tests
   in triggers-extended.test.ts:
   - trigger-store persistNow() path (lines 159-160)
   - readTriggerLogTail catch path via EISDIR (line 329)
   - readTriggerLogTail truncated:true path (line 327)
   - fireWakeUp dispatch-error catch path (line 394 of triggers.ts)

All 18 extended tests pass. typecheck βœ… Β· format:check βœ….

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(triggers): add N22 branch coverage for TALON_FIRE, empty-output, and store helpers

Ubuntu Node 22 (V8 v11) instruments more branches than Node 24 (V8 v12),
causing coverage to land at 59.67% on N22 while passing on N24.

Added 7 new tests to triggers-extended.test.ts:
- Empty stdout: covers fireWake's `trimmed ? ... : (no output)` false branch
- Mid-run TALON_FIRE: covers handleStdoutLine true branch + fireWake
  terminal=false ("signalled" header path)
- validateLanguage false paths: includes() false + typeof short-circuit
- sanitizeChatId with special chars: confirms replace() regex path
- languageExtension direct call: covers all 3 switch arms explicitly
- getTriggerByName returning undefined: covers find() undefined path

Also imports getTriggerByName, validateLanguage, sanitizeChatId, and
languageExtension from trigger-store for direct testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(triggers): comprehensive branch coverage round 4 β€” target Node 22 CI pass

Add 15 new tests across 4 test files to cover previously-unreachable branches,
specifically the ones V8 v11 (Node 22) counts but were uncovered since hb #179:

trigger-store.test.ts (+9 tests):
- loadTriggers: file-not-present, non-object JSON, corrupt primary (3 variants)
- Backup parses as null (line 94 ternary false arm)
- save() catch: Error throw and non-Error string throw (both instanceof arms)
- readTriggerLogTail: non-Error throw (false arm of err instanceof Error)

triggers-extended.test.ts (+5 new describe blocks):
- fireWake with undefined payload (payload ?? '' false arm)
- fireCount undefined treated as 0 (fireCount ?? 0 false arm)
- finalizeExit with null exit code β€” signal kill (code ?? undefined false arm)
- handleStdoutLine with no lineBuffer entry (pushBufferLine if(!buf) true arm)
- shutdownTriggers when no children running (if(children.size===0) true arm)
- Child process error event handler (line 152 handler covered)
- Timeout timer fires after child already exited (if(!c) return true arm)

cleanup-registry.test.ts (+1 test):
- Handler throws a non-Error string (err instanceof Error false arm)

mcp-launcher.test.ts (+1 test):
- ensureLauncher throws when file does not exist (if(!existsSync) true arm)
  Uses vi.doMock to intercept node:fs before fresh module import.

Node 24 local result: 60.26% (2146/3561). Previous Node 22 CI: 59.73%.
1744 tests, all passing. tsc clean, prettier clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(triggers): 5 Copilot review fixes β€” byte-safe truncation, persist on terminal, etc.

Address 5 of the 6 Copilot review threads on PR #96. The 6th (mid-run
TriggerStatus rename) is a larger semantic refactor and deferred to a
later commit; this commit pairs the smallest reasonable fix at each
site with regression tests where they're cleanly testable.

1. src/core/triggers.ts:128 β€” log WriteStream missing error handler.
   createWriteStream() with no .on("error") emits an unhandled
   EventEmitter `error` event if the log file becomes unwritable
   (disk full, permission flip), which crashes the Node process. Added
   a logError-routing handler so disk-tier failures degrade gracefully
   without killing the supervisor.

2. src/storage/trigger-store.ts:60 β€” fireCount doc/semantics mismatch.
   The doc-comment claimed "Number of mid-run TALON_FIRE: lines" but
   the supervisor increments fireCount for every wake (mid-run and
   terminal). Fixed the comment to match the actual semantics; same
   counter, accurate meaning. Avoids breaking any caller that already
   relies on fireCount as a total.

3. src/core/triggers.ts:fireWake β€” mid-run prompt says "Status: fired"
   even when terminal=false, which can mislead downstream handling
   into treating an in-flight watcher as a completed run. Decoupled
   the prompt-display status from the on-disk TriggerStatus enum:
   non-terminal fires now show "Status: signalled" in both the header
   and the "Status:" line. Terminal fires unchanged.

4. src/core/triggers.ts:bufferAsPayload + fireWake β€” FIRE_PAYLOAD_MAX_BYTES
   truncation used String.prototype.slice (UTF-16 code units), so a
   payload of N multi-byte characters could exceed the documented byte
   cap and split a character mid-codepoint. Introduced truncateUtf8Tail/
   truncateUtf8Head helpers that encode to UTF-8 bytes, slice on a byte
   boundary, then walk over UTF-8 continuation bytes (10xxxxxx) so we
   never cut a multi-byte sequence in half. The byte cap is now actually
   enforced as bytes.

5. src/storage/trigger-store.ts:282 + supervisor β€” updateTrigger only
   marks the store dirty (10s autosave window). For terminal status
   transitions (errored/cancelled/timed_out/fired) a crash inside that
   window would leave on-disk status as "running", which loadTriggers()
   then misclassifies as "terminated by previous restart". Added
   persistNow() calls at the four supervisor transition sites:
   failTrigger, cancelTrigger, the hard-timeout handler, and the
   finalizeExit path. The on-disk store now always agrees with the
   in-memory state across terminal transitions.

6. src/core/gateway-actions.ts:trigger_create β€” always returned
   "Status: running" even when spawnTrigger() failed without throwing
   (unsupported language slipping past validation, child.pid undefined,
   failTrigger() routing). With #5 above, the store now reflects the
   real state synchronously, so we re-read getTrigger(id) after spawn
   and return ok:false + lastError when it landed in "errored", or
   surface the actual status string otherwise. Callers no longer get
   false success responses.

Regression tests (triggers-extended.test.ts):
- mid-run prompt asserts "Status: signalled" appears and "Status: fired"
  does NOT, on a still-running trigger. Covers fix #3.
- multi-byte UTF-8 payload (2000Γ— πŸ’§ = 8000 bytes vs 4000 string length)
  asserts the prompt's byte length stays bounded AND contains no
  Unicode replacement character (U+FFFD), which would indicate a
  mid-codepoint split. Covers fix #4.

The remaining Copilot thread (introducing a dedicated non-terminal
status enum value, replacing the TriggerStatus|"signalled" display-
status hack) is a wider type change with API/store implications;
better tackled in a follow-up than bundled into a defensive sweep.

Verification:
- npx vitest run β†’ 1747/1748 pass (the 1 failure is the pre-existing
  package.functional "Stopped" expectation that fails whenever a live
  Talon daemon is running on the host β€” same env-dependent failure
  noted in PR #144 and PR #90's verification blocks).
- npx vitest run src/__tests__/triggers* src/__tests__/trigger-store* β†’
  74/74 pass (35 in triggers-extended including 2 new, 22 in
  trigger-store, 17 in triggers).
- npx tsc --noEmit β†’ clean.
- npx prettier --check on all 4 changed files β†’ clean.
- npm run lint β†’ 0 errors in changed files (10 pre-existing warnings
  elsewhere unchanged).

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: claudiusthebot <claudius@anthropic.com>
dylanneve1 pushed a commit that referenced this pull request May 19, 2026
…lper (#196)

Four handlers (claude-sdk, kilo, opencode, codex) inlined the same
~30-line `classify` β†’ `classifyRetry` β†’ `reset_and_retry` /
`fallback_model` / `propagate` block. Same logic, same shape, only
the log-line prefix differed. This was item #4-shaped duplication
called out in the May 16 backend review.

## What's shared now

New `src/backend/shared/handle-retry.ts` exports
`applyRetryDecision({err, chatId, activeModel, retried, params,
recurseWithRetried, backendLabel?, resetNoun?})`. Owns:

  - `classify(err)` + per-reason metric increment
  - `classifyRetry` decision
  - `reset_and_retry` β†’ `resetSession` + recurse
  - `fallback_model` β†’ transient `setChatModel` + recurse + restore in `finally`
  - `propagate` β†’ return classified error for caller to throw

Returns `{retry?, classified}` so each handler stays in charge of
its own throw/return β€” the helper just owns the recovery side
effects.

## Each backend

Four handlers drop their inline decision block + the now-unused
`classify` / `classifyRetry` / `resetSession` / `setChatModel` /
`getChatSettings` imports. Codex preserves its own
`maybeFallbackForChatGptMismatch` helper above this call site
(backend-specific recovery runs FIRST). Codex also opts into
`resetNoun: "thread"` for the log line; others use the default
`"session"`.

## Tests

New `src/__tests__/shared-handle-retry.test.ts` (8 tests):
  - propagate when err non-retryable
  - propagate when already retried
  - reset+recurse on session_expired (verifies session reset)
  - reset+recurse on context_length
  - fallback-model transient swap (verifies model active during
    recursion + restored after)
  - fallback-model restores even when recursion throws
  - propagate when retryable but no fallback registered
  - smoke test: omitted backendLabel doesn't throw

Existing handler-level integration tests (codex-handler covers all
four recovery paths end-to-end; kilo/opencode have their own
coverage) all still pass β€” the helper is exercised through them too.

## Verified

  - `npx tsc --noEmit` clean
  - `npm test` β€” 2404 passing, 12 skipped (live), 0 failing
    (was 2396; +8 new helper tests)
  - `npm run lint` β€” 11 warnings, 0 errors (all pre-existing in
    `src/frontend/discord/`)
  - `npm run format:check` clean

## Net impact

  -88 LOC of duplicated handler code, +142 LOC of shared helper +
  291 LOC of tests. The duplication delta is the win: future
  retry-logic tweaks land in one file instead of four.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant