Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Separate preservation, searchability, and promotability lifecycle flags."""

from __future__ import annotations

from alembic import op


revision = "20260410_0049"
down_revision = "20260410_0048"
branch_labels = None
depends_on = None

_UPGRADE_STATEMENTS = (
"ALTER TABLE continuity_objects ADD COLUMN is_preserved boolean NOT NULL DEFAULT TRUE",
"ALTER TABLE continuity_objects ADD COLUMN is_searchable boolean NOT NULL DEFAULT TRUE",
"ALTER TABLE continuity_objects ADD COLUMN is_promotable boolean NOT NULL DEFAULT TRUE",
(
"UPDATE continuity_objects "
"SET is_searchable = CASE WHEN object_type = 'Note' THEN FALSE ELSE TRUE END, "
" is_promotable = CASE "
" WHEN object_type IN ('Decision', 'Commitment', 'WaitingFor', 'Blocker', 'NextAction') THEN TRUE "
" ELSE FALSE "
" END"
),
(
"CREATE INDEX continuity_objects_user_searchable_updated_idx "
"ON continuity_objects (user_id, is_searchable, updated_at DESC, id DESC)"
),
(
"CREATE INDEX continuity_objects_user_promotable_updated_idx "
"ON continuity_objects (user_id, is_promotable, updated_at DESC, id DESC)"
),
)

_DOWNGRADE_STATEMENTS = (
"DROP INDEX IF EXISTS continuity_objects_user_promotable_updated_idx",
"DROP INDEX IF EXISTS continuity_objects_user_searchable_updated_idx",
"ALTER TABLE continuity_objects DROP COLUMN IF EXISTS is_promotable",
"ALTER TABLE continuity_objects DROP COLUMN IF EXISTS is_searchable",
"ALTER TABLE continuity_objects DROP COLUMN IF EXISTS is_preserved",
)


def _execute_statements(statements: tuple[str, ...]) -> None:
for statement in statements:
op.execute(statement)


def upgrade() -> None:
_execute_statements(_UPGRADE_STATEMENTS)


def downgrade() -> None:
_execute_statements(_DOWNGRADE_STATEMENTS)
114 changes: 114 additions & 0 deletions apps/api/src/alicebot_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from alicebot_api.cli_formatting import (
format_capture_output,
format_lifecycle_detail_output,
format_lifecycle_list_output,
format_open_loops_output,
format_recall_output,
format_resume_output,
Expand All @@ -27,6 +29,16 @@
ContinuityCaptureValidationError,
capture_continuity_input,
)
from alicebot_api.continuity_objects import (
default_continuity_promotable,
default_continuity_searchable,
)
from alicebot_api.continuity_lifecycle import (
ContinuityLifecycleNotFoundError,
ContinuityLifecycleValidationError,
get_continuity_lifecycle_state,
list_continuity_lifecycle_state,
)
from alicebot_api.continuity_open_loops import (
ContinuityOpenLoopValidationError,
compile_continuity_open_loop_dashboard,
Expand All @@ -49,18 +61,21 @@
from alicebot_api.contracts import (
CONTINUITY_CAPTURE_EXPLICIT_SIGNALS,
CONTINUITY_CORRECTION_ACTIONS,
DEFAULT_CONTINUITY_LIFECYCLE_LIMIT,
DEFAULT_CONTINUITY_OPEN_LOOP_LIMIT,
DEFAULT_CONTINUITY_RECALL_LIMIT,
DEFAULT_CONTINUITY_RESUMPTION_OPEN_LOOP_LIMIT,
DEFAULT_CONTINUITY_RESUMPTION_RECENT_CHANGES_LIMIT,
DEFAULT_CONTINUITY_REVIEW_LIMIT,
MAX_CONTINUITY_OPEN_LOOP_LIMIT,
MAX_CONTINUITY_RECALL_LIMIT,
MAX_CONTINUITY_LIFECYCLE_LIMIT,
MAX_CONTINUITY_RESUMPTION_OPEN_LOOP_LIMIT,
MAX_CONTINUITY_RESUMPTION_RECENT_CHANGES_LIMIT,
MAX_CONTINUITY_REVIEW_LIMIT,
ContinuityCaptureCreateInput,
ContinuityCorrectionInput,
ContinuityLifecycleQueryInput,
ContinuityOpenLoopDashboardQueryInput,
ContinuityRecallQueryInput,
ContinuityResumptionBriefRequestInput,
Expand Down Expand Up @@ -176,6 +191,26 @@ def _run_recall(ctx: CLIContext, args: argparse.Namespace) -> str:
return format_recall_output(payload)


def _run_lifecycle_list(ctx: CLIContext, args: argparse.Namespace) -> str:
with _store_context(ctx) as store:
payload = list_continuity_lifecycle_state(
store,
user_id=ctx.user_id,
request=ContinuityLifecycleQueryInput(limit=args.limit),
)
return format_lifecycle_list_output(payload)


def _run_lifecycle_show(ctx: CLIContext, args: argparse.Namespace) -> str:
with _store_context(ctx) as store:
payload = get_continuity_lifecycle_state(
store,
user_id=ctx.user_id,
continuity_object_id=args.continuity_object_id,
)
return format_lifecycle_detail_output(payload)


def _run_resume(ctx: CLIContext, args: argparse.Namespace) -> str:
with _store_context(ctx) as store:
payload = compile_continuity_resumption_brief(
Expand All @@ -191,6 +226,7 @@ def _run_resume(ctx: CLIContext, args: argparse.Namespace) -> str:
until=args.until,
max_recent_changes=args.max_recent_changes,
max_open_loops=args.max_open_loops,
include_non_promotable_facts=args.include_non_promotable_facts,
),
)
return format_resume_output(payload)
Expand Down Expand Up @@ -286,6 +322,10 @@ def _run_status(ctx: CLIContext, _args: argparse.Namespace) -> str:
"continuity_objects_stale": 0,
"continuity_objects_superseded": 0,
"continuity_objects_deleted": 0,
"continuity_objects_searchable": 0,
"continuity_objects_non_searchable": 0,
"continuity_objects_promotable": 0,
"continuity_objects_non_promotable": 0,
"review_correction_ready": 0,
"review_active": 0,
"review_stale": 0,
Expand Down Expand Up @@ -343,6 +383,46 @@ def _run_status(ctx: CLIContext, _args: argparse.Namespace) -> str:
"continuity_objects_stale": object_status_counts["stale"],
"continuity_objects_superseded": object_status_counts["superseded"],
"continuity_objects_deleted": object_status_counts["deleted"],
"continuity_objects_searchable": sum(
1
for candidate in recall_candidates
if bool(
candidate.get(
"is_searchable",
default_continuity_searchable(str(candidate["object_type"])),
)
)
),
"continuity_objects_non_searchable": sum(
1
for candidate in recall_candidates
if not bool(
candidate.get(
"is_searchable",
default_continuity_searchable(str(candidate["object_type"])),
)
)
),
"continuity_objects_promotable": sum(
1
for candidate in recall_candidates
if bool(
candidate.get(
"is_promotable",
default_continuity_promotable(str(candidate["object_type"])),
)
)
),
"continuity_objects_non_promotable": sum(
1
for candidate in recall_candidates
if not bool(
candidate.get(
"is_promotable",
default_continuity_promotable(str(candidate["object_type"])),
)
)
),
"review_correction_ready": review_counts["active"] + review_counts["stale"],
"review_active": review_counts["active"],
"review_stale": review_counts["stale"],
Expand Down Expand Up @@ -403,6 +483,26 @@ def build_parser() -> argparse.ArgumentParser:
)
recall_parser.set_defaults(handler=_run_recall)

lifecycle_parser = subparsers.add_parser("lifecycle", help="Inspect continuity lifecycle state.")
lifecycle_subparsers = lifecycle_parser.add_subparsers(dest="lifecycle_command", required=True)

lifecycle_list_parser = lifecycle_subparsers.add_parser("list", help="List lifecycle states.")
lifecycle_list_parser.add_argument(
"--limit",
type=int,
default=DEFAULT_CONTINUITY_LIFECYCLE_LIMIT,
help=f"Max lifecycle results (1-{MAX_CONTINUITY_LIFECYCLE_LIMIT}).",
)
lifecycle_list_parser.set_defaults(handler=_run_lifecycle_list)

lifecycle_show_parser = lifecycle_subparsers.add_parser("show", help="Show one lifecycle state.")
lifecycle_show_parser.add_argument(
"continuity_object_id",
type=_parse_uuid,
help="Continuity object UUID.",
)
lifecycle_show_parser.set_defaults(handler=_run_lifecycle_show)

resume_parser = subparsers.add_parser("resume", help="Compile continuity resumption brief.")
_add_scope_filter_arguments(resume_parser)
resume_parser.add_argument(
Expand All @@ -417,6 +517,11 @@ def build_parser() -> argparse.ArgumentParser:
default=DEFAULT_CONTINUITY_RESUMPTION_OPEN_LOOP_LIMIT,
help=f"Open loop limit (0-{MAX_CONTINUITY_RESUMPTION_OPEN_LOOP_LIMIT}).",
)
resume_parser.add_argument(
"--include-non-promotable-facts",
action="store_true",
help="Include searchable but non-promotable facts in recent changes.",
)
resume_parser.set_defaults(handler=_run_resume)

open_loops_parser = subparsers.add_parser(
Expand Down Expand Up @@ -522,6 +627,13 @@ def _validate_arguments(args: argparse.Namespace) -> None:
minimum=1,
maximum=MAX_CONTINUITY_RECALL_LIMIT,
)
elif args.command == "lifecycle" and args.lifecycle_command == "list":
_validate_limit(
args.limit,
option_name="--limit",
minimum=1,
maximum=MAX_CONTINUITY_LIFECYCLE_LIMIT,
)
elif args.command == "resume":
_validate_limit(
args.max_recent_changes,
Expand Down Expand Up @@ -564,6 +676,8 @@ def main(argv: list[str] | None = None) -> int:
ValueError,
psycopg.Error,
ContinuityCaptureValidationError,
ContinuityLifecycleValidationError,
ContinuityLifecycleNotFoundError,
ContinuityRecallValidationError,
ContinuityResumptionValidationError,
ContinuityOpenLoopValidationError,
Expand Down
Loading
Loading