Skip to content

feat(errors): add SysModulesDisabledError typed error class#29

Merged
tercel merged 6 commits into
mainfrom
sys-modules-disabled-error
May 22, 2026
Merged

feat(errors): add SysModulesDisabledError typed error class#29
tercel merged 6 commits into
mainfrom
sys-modules-disabled-error

Conversation

@tercel
Copy link
Copy Markdown
Contributor

@tercel tercel commented May 22, 2026

Summary

Replaces bare RuntimeError raises in APCore.on / off / disable / enable with a typed SysModulesDisabledError (code SYS_MODULES_DISABLED, retryable=False).

Callers can now dispatch on error.code == "SYS_MODULES_DISABLED" or except SysModulesDisabledError instead of except Exception + string matching.

Cross-language equivalent of Rust's ErrorCode::SysModulesDisabled and a parallel TypeScript SysModulesDisabledError landing alongside this PR in apcore-typescript.

Test plan

  • pytest tests/test_client.py tests/test_public_api.py -q111 passed in 0.15s
  • New error class exported from apcore top-level (__init__.py)
  • retryable=False default — sys_modules state is config-time, not retry-fixable

tercel added 5 commits May 21, 2026 14:00
Per apcore/CLAUDE.md, implementation repos contain only code and a
README — feature specs, test-case matrices, and design notes live in
the apcore protocol-spec repo. Removes:

  - docs/features/async-task-evolution.md
  - docs/features/middleware-architecture-hardening.md
  - docs/async-task-evolution/test-cases.md

These files were also stale (referenced the deprecated TaskStore.put
method and the removed TaskStatus.RETRYING enum value); the canonical
sources are the implementation in src/apcore/async_task.py and the
upstream spec at apcore/docs/features/async-tasks.md.

Signed-off-by: tercel <tercel.yi@gmail.com>
…nager surface

Implements four sync-audit findings against v0.22.0:

  * A-D-AT-04 / D-17 — TaskStore Protocol methods (save, get, delete,
    list, list_expired) are now async on the Protocol and on the default
    InMemoryTaskStore. AsyncTaskManager awaits all store calls; a
    transitional shim still drives sync stores so external custom stores
    get one deprecation window to migrate. cleanup() is async because
    list/delete now are.

  * A-D-AT-03 / D-11 — ReaperHandle.stop and AsyncTaskManager.stop_reaper
    are async and drain the reaper task before returning. Callers no
    longer need a manual `await asyncio.sleep(0)` after `handle.stop()`.

  * A-D-AT-06 — get_status / list_tasks return shallow copies via
    dataclasses.replace so callers cannot mutate the live store record.
    New get_status_async / list_tasks_async surfaces for I/O-backed
    stores that genuinely suspend.

  * A-D-AT-09 / D-14 — Legacy RetryPolicy now defaults max_retries to 0
    (retries are strictly opt-in across all SDKs) and emits a
    DeprecationWarning on instantiation steering callers to RetryConfig.

Adds tests/test_async_task_sync_audit_v022.py pinning each invariant
plus an async-only custom-store smoke test. Updates existing tests to
the new async surface (await store ops, await stop_reaper, await
cleanup) and registers an existing sync test as `@pytest.mark.asyncio`
where required.

Signed-off-by: tercel <tercel.yi@gmail.com>
…wrap

Two normative-decision fixes for the executor pipeline:

  * A-D-EXEC-002 / D-21 — BuiltinCallChainGuard now observes
    cancel_token.is_cancelled before any guard work and short-circuits
    with ExecutionCancelledError. Combined with the existing Step 8
    check this satisfies the two-point cancel-token invariant; the
    earlier single-check implementation leaked compute through
    ACL/middleware/validation even when the caller had already
    cancelled.

  * A-D-EXEC-005 / D-22 — Executor._recover_from_call_error now unwraps
    MiddlewareChainError and propagates its `.original` typed cause
    unchanged. Previously the wrapper was collapsed to a generic
    ModuleExecuteError, breaking callers that dispatch on typed errors
    (notably MCP/A2A bridges keying on APPROVAL_DENIED vs
    MODULE_EXECUTE_ERROR). Mirrors the TypeScript and Rust SDKs.

Adds tests/test_executor_sync_audit_v022.py with regression coverage
for both decisions. Drops the now-unused ModuleExecuteError import
from executor.py.

Signed-off-by: tercel <tercel.yi@gmail.com>
…ll paths

Four sync-audit findings clamp the Issue #65 invariants so they hold
uniformly across every registration path, not just the public
register() API:

  * A-D-REG-002 — register_internal's ephemeral-namespace check now
    uses the shared _is_ephemeral helper instead of bare .startswith,
    so the bare ID "ephemeral" is rejected too (it previously slipped
    through the .startswith("ephemeral.") guard).

  * A-D-REG-003 — _register_in_order (discover path) now follows the
    same three-phase deferred-publish protocol as register(): reserve
    in-flight slot → run on_load outside the lock → publish on
    success. Previously the discover path published into _modules
    BEFORE invoking on_load, leaving a window in which registry.get()
    callers could observe a module whose on_load-installed state was
    incomplete.

  * A-D-REG-004 — register_internal now uses the same three-phase
    protocol. The Issue #65 invariant ("modules invisible until
    on_load completes") now holds for sys-modules too.

  * A-D-REG-005 — _invoke_on_load now emits
    apcore.registry.module_load_failed when on_load fails, mirroring
    the public register() path so subscribers have a single hook for
    partial-init detection regardless of which registration route was
    used.

Adds tests/test_registry_sync_audit_v022.py with regression coverage
for all four findings, including a probe module whose on_load asserts
the module is NOT yet visible in the registry while the callback runs.

Signed-off-by: tercel <tercel.yi@gmail.com>
Replaces bare RuntimeError raises in APCore.on/off/disable/enable with a
typed error class so callers can dispatch on
error.code == "SYS_MODULES_DISABLED" rather than catching the generic
Exception base.

Cross-language equivalent of Rust's ErrorCode::SysModulesDisabled and
the parallel TypeScript SysModulesDisabledError landing alongside this
PR. The class defaults retryable=False — sys_modules state is a
config-time decision, not a transient condition retries can fix.

Test plan
- pytest tests/test_client.py tests/test_public_api.py -q
  → 111 passed in 0.15s

Signed-off-by: tercel <tercel.yi@gmail.com>
@tercel tercel merged commit ccadc85 into main May 22, 2026
1 of 2 checks passed
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