Compare commits

...
Author SHA1 Message Date
m aa01e8e99b DEV-000065 final project status
CI / test (pull_request) Successful in 15s
CI / test (push) Successful in 21s
2026-08-17 16:43:33 +03:00
m 1113ced623 DEV-000065 answer attention from canonical today
CI / test (push) Successful in 15s
Promote DEV-000065 canonical attention chat answer.
2026-08-17 13:41:59 +00:00
m f3ff430812 DEV-000065 answer attention from canonical today
CI / test (pull_request) Successful in 18s
CI / test (push) Successful in 14s
2026-08-17 16:40:59 +03:00
m 2ec53e40cf DEV-000065 fix notification binding origin
CI / test (push) Successful in 19s
Promote DEV-000065 live wiring fix.
2026-08-17 13:38:11 +00:00
m 18fbc7f105 DEV-000065 fix notification binding origin
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 15s
2026-08-17 16:33:05 +03:00
m a0dce634f3 DEV-000065 assistant presence attention surface
CI / test (push) Successful in 14s
Promote DEV-000065 ADR-0014 assistant presence, attention, and human surface.
2026-08-17 13:23:48 +00:00
m 5d4e064a94 DEV-000065 assistant presence attention surface
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 15s
2026-08-17 16:23:16 +03:00
m 6e73318e49 Merge pull request #41 from codex/dev000064-final-status
CI / test (push) Successful in 14s
DEV-000064 final production status
2026-08-17 12:58:41 +00:00
m 1519af3310 DEV-000064 final production status
CI / test (push) Successful in 14s
CI / test (pull_request) Successful in 14s
2026-08-17 15:57:52 +03:00
m 8fe596264e Merge pull request #40 from codex/dev000064-legacy-reminder-migration-fix
CI / test (push) Successful in 19s
DEV-000064 legacy reminder migration compatibility
2026-08-17 12:55:19 +00:00
m ee51789ee2 DEV-000064 allow legacy prospective metadata
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 18s
2026-08-17 15:54:25 +03:00
m a083bac65f Merge pull request #39 from codex/dev000064-capability-resolution-selection-gap-foundation
CI / test (push) Successful in 17s
DEV-000064 capability resolution foundation
2026-08-17 12:52:40 +00:00
m 9a84ac942c DEV-000064 capability resolution foundation
CI / test (push) Successful in 15s
CI / test (pull_request) Successful in 14s
2026-08-17 15:51:25 +03:00
m fe531ab56f Merge DEV-000063 final production status
CI / test (push) Successful in 13s
Status-only completion record for DEV-000063.
2026-08-17 12:19:09 +00:00
m d8f994e07e DEV-000063 record completed production status
CI / test (push) Successful in 14s
CI / test (pull_request) Successful in 14s
2026-08-17 15:18:11 +03:00
m 9d50a3ec63 Merge DEV-000063 speech act follow-up
CI / test (push) Successful in 14s
Speech act classifier follow-up for DEV-000063.
2026-08-17 12:15:55 +00:00
m 2d142cdfdb DEV-000063 classify reminder and capability directives
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 19s
2026-08-17 15:14:56 +03:00
m 06b6cc3f9d Merge DEV-000063 Intent to Work Planning Foundation
CI / test (push) Successful in 20s
Promote DEV-000063 rev.1 to main.
2026-08-17 12:11:45 +00:00
43 changed files with 3169 additions and 32 deletions
+112
View File
@@ -14,6 +14,7 @@ import (
var ErrCapabilityHealthNotFound = errors.New("capability health not found")
var ErrCapabilityBindingNotFound = errors.New("capability binding not found")
var ErrCapabilityBindingConflict = errors.New("capability binding conflict")
var ErrCapabilityImplementationNotFound = errors.New("capability implementation not found")
type CapabilityRepository struct{ pool *pgxpool.Pool }
@@ -106,6 +107,117 @@ func (r *CapabilityRepository) SaveBinding(ctx context.Context, binding capabili
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SaveCapabilityImplementation(ctx context.Context, value capability.CapabilityImplementation) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
_, err = tx.Exec(ctx, `INSERT INTO capability.capability_implementations (
implementation_id,capability_id,capability_kind,owner_principal_id,revision,implementation_kind,target_ref,credential_ref,
qualification,activation,health,authority_policy_ref,data_constraints,cost_score_millis,latency_score_millis,reliability_millis,observed_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT DO NOTHING`,
value.ID, value.CapabilityID, value.CapabilityKind, value.OwnerPrincipalID, value.Revision, value.Kind, value.TargetRef,
value.CredentialRef, value.Qualification, value.Activation, value.Health, value.AuthorityPolicyRef, value.DataConstraints,
value.CostScoreMillis, value.LatencyScoreMillis, value.ReliabilityMillis, value.ObservedAtUnixMillis)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.capability_implementation_heads(implementation_id,revision,capability_kind,owner_principal_id)
VALUES ($1,$2,$3,$4)
ON CONFLICT (implementation_id) DO UPDATE SET revision=EXCLUDED.revision,capability_kind=EXCLUDED.capability_kind,owner_principal_id=EXCLUDED.owner_principal_id,updated_at=CURRENT_TIMESTAMP
WHERE capability.capability_implementation_heads.revision < EXCLUDED.revision`, value.ID, value.Revision, value.CapabilityKind, value.OwnerPrincipalID)
if err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *CapabilityRepository) LoadCapabilityImplementations(ctx context.Context, kind capability.CapabilityKind, owner identity.PrincipalID) ([]capability.CapabilityImplementation, error) {
rows, err := r.pool.Query(ctx, `SELECT
i.implementation_id,i.capability_id,i.capability_kind,i.owner_principal_id,i.revision,i.implementation_kind,i.target_ref,COALESCE(i.credential_ref,''),
i.qualification,i.activation,i.health,i.authority_policy_ref,i.data_constraints,i.cost_score_millis,i.latency_score_millis,i.reliability_millis,i.observed_at_unix_millis
FROM capability.capability_implementation_heads h
JOIN capability.capability_implementations i ON i.implementation_id=h.implementation_id AND i.revision=h.revision
WHERE i.capability_kind=$1 AND i.owner_principal_id=$2
ORDER BY i.implementation_id`, kind, owner)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]capability.CapabilityImplementation, 0)
for rows.Next() {
var value capability.CapabilityImplementation
if err = rows.Scan(&value.ID, &value.CapabilityID, &value.CapabilityKind, &value.OwnerPrincipalID, &value.Revision, &value.Kind, &value.TargetRef,
&value.CredentialRef, &value.Qualification, &value.Activation, &value.Health, &value.AuthorityPolicyRef, &value.DataConstraints,
&value.CostScoreMillis, &value.LatencyScoreMillis, &value.ReliabilityMillis, &value.ObservedAtUnixMillis); err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
func (r *CapabilityRepository) AvailableCapabilityKinds(ctx context.Context, owner identity.PrincipalID) ([]capability.CapabilityKind, error) {
rows, err := r.pool.Query(ctx, `SELECT DISTINCT i.capability_kind
FROM capability.capability_implementation_heads h
JOIN capability.capability_implementations i ON i.implementation_id=h.implementation_id AND i.revision=h.revision
WHERE i.owner_principal_id=$1 AND i.qualification=$2 AND i.activation=$3 AND i.health=$4
ORDER BY i.capability_kind`, owner, capability.QualificationValidated, capability.ActivationActive, capability.HealthAvailable)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]capability.CapabilityKind, 0)
for rows.Next() {
var kind capability.CapabilityKind
if err := rows.Scan(&kind); err != nil {
return nil, err
}
result = append(result, kind)
}
return result, rows.Err()
}
func (r *CapabilityRepository) SaveCapabilityGap(ctx context.Context, value capability.CapabilityGap) error {
strategies := make([]string, 0, len(value.Strategies))
for _, strategy := range value.Strategies {
strategies = append(strategies, string(strategy.Kind))
}
_, err := r.pool.Exec(ctx, `INSERT INTO capability.capability_gaps (
gap_id,requirement_id,capability_kind,capability_id,owner_principal_id,original_intent_id,source_utterance_id,
reason,detail,strategies,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, value.ID, value.RequirementID,
value.CapabilityKind, value.CapabilityID, value.OwnerPrincipalID, value.OriginalIntentID, value.SourceUtteranceID,
value.Reason, value.Detail, strategies, value.RecordedAtUnixMillis)
return err
}
func (r *CapabilityRepository) LatestCapabilityGap(ctx context.Context, kind capability.CapabilityKind, owner identity.PrincipalID) (capability.CapabilityGap, error) {
var value capability.CapabilityGap
var strategies []string
err := r.pool.QueryRow(ctx, `SELECT gap_id,requirement_id,capability_kind,capability_id,owner_principal_id,original_intent_id,source_utterance_id,reason,detail,strategies,recorded_at_unix_millis
FROM capability.capability_gaps WHERE capability_kind=$1 AND owner_principal_id=$2 ORDER BY recorded_at_unix_millis DESC, gap_id DESC LIMIT 1`, kind, owner).
Scan(&value.ID, &value.RequirementID, &value.CapabilityKind, &value.CapabilityID, &value.OwnerPrincipalID, &value.OriginalIntentID, &value.SourceUtteranceID, &value.Reason, &value.Detail, &strategies, &value.RecordedAtUnixMillis)
if err != nil {
return capability.CapabilityGap{}, err
}
for _, strategy := range strategies {
value.Strategies = append(value.Strategies, capability.AcquisitionStrategyCandidate{Kind: capability.AcquisitionStrategyKind(strategy)})
}
return value, nil
}
func (r *CapabilityRepository) SaveCapabilityUsageObservation(ctx context.Context, value capability.CapabilityUsageObservation) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.capability_usage_observations (
observation_id,implementation_id,implementation_revision,capability_id,capability_kind,owner_principal_id,
original_intent_id,source_utterance_id,outcome,outcome_ref,latency_millis,observed_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, value.ID, value.ImplementationID,
value.ImplementationRevision, value.CapabilityID, value.CapabilityKind, value.OwnerPrincipalID, value.OriginalIntentID,
value.SourceUtteranceID, value.Outcome, value.OutcomeRef, value.LatencyMillis, value.ObservedAtUnixMillis)
return err
}
func (r *CapabilityRepository) LoadBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) {
var binding capability.Binding
err := r.pool.QueryRow(ctx, `SELECT binding_id,capability_id,owner_principal_id,revision,state,https_origin,delivery_path,receipt_path,
+211 -6
View File
@@ -19,6 +19,9 @@ var (
ErrProspectiveRevisionConflict = errors.New("prospective item revision conflict")
ErrActionReceiptNotFound = errors.New("continuity action receipt not found")
ErrActionIntentConflict = errors.New("action intent was already used for a different command")
ErrAttentionNotFound = errors.New("attention item not found")
ErrAttentionStateConflict = errors.New("attention item state conflict")
ErrAttentionRevisionConflict = errors.New("attention item revision conflict")
)
type ContinuityConfig struct{ Host, Port, Database, User, Password string }
@@ -34,12 +37,12 @@ func OpenContinuity(ctx context.Context, config ContinuityConfig) (*ContinuityRe
}
func (r *ContinuityRepository) Close() { r.pool.Close() }
func (r *ContinuityRepository) Save(ctx context.Context, item continuity.ProspectiveItem) error {
_, err := r.pool.Exec(ctx, `INSERT INTO continuity.prospective_items (prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (prospective_item_id) DO UPDATE SET state=EXCLUDED.state, revision=EXCLUDED.revision`, item.ID, item.CommitmentID, item.OwnerPrincipalID, item.ScopeID, item.GoalID, item.TriggerAtUnixMillis, item.State, item.Revision)
_, err := r.pool.Exec(ctx, `INSERT INTO continuity.prospective_items (prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (prospective_item_id) DO UPDATE SET state=EXCLUDED.state, revision=EXCLUDED.revision`, item.ID, item.CommitmentID, item.OwnerPrincipalID, item.ScopeID, item.GoalID, item.SourceIntentID, item.SourceUtteranceID, item.Summary, item.TriggerAtUnixMillis, item.State, item.Revision)
return err
}
func (r *ContinuityRepository) Load(ctx context.Context, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
var item continuity.ProspectiveItem
err := r.pool.QueryRow(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE prospective_item_id=$1`, id).Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.TriggerAtUnixMillis, &item.State, &item.Revision)
err := r.pool.QueryRow(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE prospective_item_id=$1`, id).Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision)
if err != nil {
return continuity.ProspectiveItem{}, fmt.Errorf("load prospective item: %w", err)
}
@@ -47,7 +50,7 @@ func (r *ContinuityRepository) Load(ctx context.Context, id continuity.Prospecti
}
func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuity.ProspectiveItem, error) {
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE owner_principal_id=$1 ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $2`, owner, limit)
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE owner_principal_id=$1 ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $2`, owner, limit)
if err != nil {
return nil, fmt.Errorf("list prospective items by owner: %w", err)
}
@@ -55,7 +58,7 @@ func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.Princ
items := make([]continuity.ProspectiveItem, 0)
for rows.Next() {
var item continuity.ProspectiveItem
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
return nil, fmt.Errorf("scan prospective item: %w", err)
}
items = append(items, item)
@@ -66,6 +69,184 @@ func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.Princ
return items, nil
}
func (r *ContinuityRepository) DueArmed(ctx context.Context, nowUnixMillis int64, limit int) ([]continuity.ProspectiveItem, error) {
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid due reminder limit")
}
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision
FROM continuity.prospective_items
WHERE state=$1 AND trigger_at_unix_millis <= $2
ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $3`, continuity.ProspectiveArmed, nowUnixMillis, limit)
if err != nil {
return nil, fmt.Errorf("list due prospective items: %w", err)
}
defer rows.Close()
items := make([]continuity.ProspectiveItem, 0)
for rows.Next() {
var item continuity.ProspectiveItem
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
return nil, fmt.Errorf("scan due prospective item: %w", err)
}
items = append(items, item)
}
return items, rows.Err()
}
func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continuity.ProspectiveItemID, revision continuity.ProspectiveRevision, attentionID continuity.AttentionItemID, nowUnixMillis int64) (continuity.ProspectiveItem, continuity.AttentionItem, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
var triggered continuity.ProspectiveItem
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4 AND trigger_at_unix_millis <= $5
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveTriggered, id, continuity.ProspectiveArmed, revision, nowUnixMillis).Scan(
&triggered.ID, &triggered.CommitmentID, &triggered.OwnerPrincipalID, &triggered.ScopeID, &triggered.GoalID,
&triggered.SourceIntentID, &triggered.SourceUtteranceID, &triggered.Summary, &triggered.TriggerAtUnixMillis, &triggered.State, &triggered.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, ErrProspectiveRevisionConflict
}
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("trigger due reminder: %w", err)
}
var revalidating continuity.ProspectiveItem
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveRevalidating, triggered.ID, continuity.ProspectiveTriggered, triggered.Revision).Scan(
&revalidating.ID, &revalidating.CommitmentID, &revalidating.OwnerPrincipalID, &revalidating.ScopeID, &revalidating.GoalID,
&revalidating.SourceIntentID, &revalidating.SourceUtteranceID, &revalidating.Summary, &revalidating.TriggerAtUnixMillis, &revalidating.State, &revalidating.Revision)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("begin due reminder revalidation: %w", err)
}
var delivered continuity.ProspectiveItem
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveDelivered, revalidating.ID, continuity.ProspectiveRevalidating, revalidating.Revision).Scan(
&delivered.ID, &delivered.CommitmentID, &delivered.OwnerPrincipalID, &delivered.ScopeID, &delivered.GoalID,
&delivered.SourceIntentID, &delivered.SourceUtteranceID, &delivered.Summary, &delivered.TriggerAtUnixMillis, &delivered.State, &delivered.Revision)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("finish due reminder revalidation: %w", err)
}
attention, err := continuitydomain.BuildDueReminderAttention(delivered, attentionID, nowUnixMillis)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
}
err = tx.QueryRow(ctx, `INSERT INTO continuity.attention_items (
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,
action_attempt_id,delivery_outcome,revision)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','',1)
ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET source_prospective_item_id=EXCLUDED.source_prospective_item_id
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
attention.ID, attention.OwnerPrincipalID, attention.ScopeID, attention.SourceKind, attention.SourceProspectiveID,
attention.Level, attention.State, attention.Summary, attention.DueAtUnixMillis, attention.CreatedAtUnixMillis).Scan(
&attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID,
&attention.Level, &attention.State, &attention.Summary, &attention.DueAtUnixMillis, &attention.CreatedAtUnixMillis,
&attention.DeliveredAtUnixMillis, &attention.ActionIntentID, &attention.AuthorizedActionID, &attention.ActionAttemptID,
&attention.DeliveryOutcome, &attention.Revision)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("create due reminder attention: %w", err)
}
if err = tx.Commit(ctx); err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
}
return delivered, attention, nil
}
func (r *ContinuityRepository) PendingAttention(ctx context.Context, limit int) ([]continuity.AttentionItem, error) {
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid attention limit")
}
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision
FROM continuity.attention_items WHERE state=$1 ORDER BY due_at_unix_millis, attention_item_id LIMIT $2`, continuity.AttentionPending, limit)
if err != nil {
return nil, fmt.Errorf("list pending attention: %w", err)
}
defer rows.Close()
return scanAttentionRows(rows)
}
func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuity.AttentionItem, error) {
if owner == "" || limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid attention owner query")
}
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision
FROM continuity.attention_items WHERE owner_principal_id=$1 ORDER BY due_at_unix_millis DESC, attention_item_id DESC LIMIT $2`, owner, limit)
if err != nil {
return nil, fmt.Errorf("list attention by owner: %w", err)
}
defer rows.Close()
return scanAttentionRows(rows)
}
func (r *ContinuityRepository) MarkAttentionDelivering(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items SET state=$1, revision=revision+1
WHERE attention_item_id=$2 AND state=$3 AND revision=$4
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
continuity.AttentionDelivering, id, continuity.AttentionPending, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
return item, err
}
func (r *ContinuityRepository) MarkAttentionDelivered(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string, deliveredAtUnixMillis int64) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items
SET state=$1, revision=revision+1, delivered_at_unix_millis=$2, action_intent_id=$3, authorized_action_id=$4,
action_attempt_id=$5, delivery_outcome=$6
WHERE attention_item_id=$7 AND state=$8 AND revision=$9
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
continuity.AttentionDelivered, deliveredAtUnixMillis, intentID, authorizedID, attemptID, outcome,
id, continuity.AttentionDelivering, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
return item, err
}
func (r *ContinuityRepository) MarkAttentionFailed(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items
SET state=$1, revision=revision+1, action_intent_id=$2, authorized_action_id=$3,
action_attempt_id=$4, delivery_outcome=$5
WHERE attention_item_id=$6 AND state=$7 AND revision=$8
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
continuity.AttentionFailed, intentID, authorizedID, attemptID, outcome,
id, continuity.AttentionDelivering, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
return item, err
}
// SatisfyDelivered owns both the Prospective CAS and its idempotency receipt in
// one Continuity-local transaction. It never reaches Commit storage.
func (r *ContinuityRepository) SatisfyDelivered(ctx context.Context, command continuity.SatisfyDeliveredCommand) (continuity.ActionReceipt, error) {
@@ -95,10 +276,10 @@ func (r *ContinuityRepository) SatisfyDelivered(ctx context.Context, command con
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND owner_principal_id=$3 AND state=$4 AND revision=$5
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,trigger_at_unix_millis,state,revision`,
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveSatisfied, command.Subject.ProspectiveItemID, command.OwnerPrincipalID,
continuity.ProspectiveDelivered, command.Subject.ExpectedRevision).Scan(
&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID,
&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary,
&item.TriggerAtUnixMillis, &item.State, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.ActionReceipt{}, diagnoseProspectiveConflict(ctx, tx, command)
@@ -135,6 +316,30 @@ type continuityRowQuerier interface {
QueryRow(context.Context, string, ...any) pgx.Row
}
type continuityRows interface {
Next() bool
Scan(...any) error
Err() error
}
func scanAttentionRows(rows continuityRows) ([]continuity.AttentionItem, error) {
items := make([]continuity.AttentionItem, 0)
for rows.Next() {
var item continuity.AttentionItem
if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID,
&item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis,
&item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID,
&item.DeliveryOutcome, &item.Revision); err != nil {
return nil, fmt.Errorf("scan attention item: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("scan attention items: %w", err)
}
return items, nil
}
func loadActionReceipt(ctx context.Context, querier continuityRowQuerier, intentID continuity.CommittedActionIntentID) (continuity.ActionReceipt, error) {
var receipt continuity.ActionReceipt
err := querier.QueryRow(ctx, `SELECT action_intent_id,authorized_action_id,owner_principal_id,prospective_item_id,
@@ -0,0 +1,33 @@
# ADR-0013 — Capability Resolution, Selection and Gap Handling Foundation
Status: ACCEPTED
Date: 2026-08-17
## Context
DEV-000063 introduced Work-owned `CapabilityRequirement` without selecting or executing concrete implementations. The system now needs a provider-neutral way to answer which real capabilities can satisfy a requirement, whether they are operationally usable, whether policy permits them, and what typed gap remains when no implementation is eligible.
The Core Semantic Model already assigns Capability registry/bindings ownership to the Capability module. Cognitive Registry is a specialized capability registry for model/provider/model-binding semantics. This ADR must not create a parallel cognitive source of truth.
## Decision
1. Keep `capability` as the canonical owner for capability descriptors, implementation registrations, implementation health/availability, resolution results, typed gaps, acquisition strategy candidates and generic capability usage observations.
2. Represent a capability separately from an implementation:
- `Capability` / `CapabilityRef`: what the system can obtain or do.
- `CapabilityImplementation`: a concrete adapter, provider, runtime, agent or internal function that may satisfy that capability.
3. Capability identity is typed but extensible. Known `CapabilityKind` constants are allowed for current semantics, but the registry stores string capability kinds and stable capability IDs without imposing a closed universe.
4. Resolver V1 returns `AVAILABLE`, `PARTIALLY_AVAILABLE`, `UNAVAILABLE` or `UNKNOWN` with eligible implementations and typed exclusion/gap reasons.
5. Selection V1 is deterministic and evidence-weighted. It must rank eligible implementations by typed evidence and stable IDs, not by hardcoded provider/adapter names.
6. CredentialRef is allowed as implementation metadata, but credential existence never means capability availability or Authority. Secret values remain outside canonical state and are resolved only by trusted runtimes.
7. Cognitive systems remain integrated through the existing Cognitive Registry and Router. General capability resolution may express the conceptual relation that cognition is a capability, but cognitive invocation continues through the existing gateway/router.
8. Reminder scheduling is the first concrete production capability for this foundation. It creates Continuity-owned `ProspectiveItem` state through the Continuity runtime boundary. It does not create durable Work for simple reminders.
9. Unsupported email sending creates a Capability-owned `CapabilityGap` correlated to the original human intent. It must not claim execution and must not become an automatic Codex/self-extension task.
## Consequences
- `capability` gains generic implementation, resolution, gap and usage-observation contracts/tables.
- `continuity` gains a narrow authenticated creator reminder-registration endpoint that owns creation of `ProspectiveItem` rows.
- `/chat` can schedule a simple reminder through capability resolution and can persist a typed gap for unavailable capabilities.
- No cognitive binding is promoted to `ACTIVE`.
- No new canonical owner, broad plugin marketplace, autonomous adapter installation or self-modifying loop is introduced.
@@ -0,0 +1,46 @@
# ADR-0014 — Assistant Presence, Attention, and Human Surface V1
Status: Accepted
Task: DEV-000065
## Context
Domovoy already records human utterances, original human intent, current focus, capability requirements, capability gaps, and Continuity-owned reminder state. Before this decision, creating a reminder only persisted an `ARMED` `ProspectiveItem`; it did not by itself return to the human when the due condition arrived.
The system already has the correct outbound mutation foundation: Commit owns authorized external actions, Capability owns notification bindings, and the notification receiver persists idempotent receipts. DEV-000065 must therefore connect due Continuity state to the existing Commit/Executor path instead of creating a parallel reminder or notification subsystem.
## Decision
1. Continuity remains the canonical owner of future obligations and prospective state.
2. Continuity also owns the V1 typed `AttentionItem` state because attention is the human-facing relevance decision derived from future obligations and other future internal events.
3. `AttentionItem` V1 is deliberately narrow: it records source, owner, level, state, summary, due time, and delivery evidence references. It is not a task manager, dashboard model, or capability registry.
4. A due reminder follows the existing prospective lifecycle: `ARMED → TRIGGERED → REVALIDATING → DELIVERED`. `DELIVERED` means the due obligation has reached the human-contact phase; it does not grant external mutation authority by itself.
5. Human notification remains a Commit-owned external action using the existing `deliver-prospective-notification-v1` operation and Capability-owned active notification binding.
6. Commit may authorize a prospective notification using a Continuity prospective item as the live justification. A Work `TaskTarget` is not mandatory for this operation when the operation is internally invoked for a validated due `ProspectiveItem`.
7. The due processing runtime may poll canonical PostgreSQL state, but correctness must come from persisted state transitions and idempotency constraints, not from process-local timers.
8. The Human Assistant web surface is separate from the Operator Console. Chat remains primary; Today/Attention and History are human-readable projections built through runtime APIs over canonical state.
## Boundaries
- Continuity writes only Continuity-owned prospective and attention state.
- Commit writes only Commit-owned action/attempt state and invokes the existing notification executor.
- Capability owns the notification binding and health/availability semantics.
- Interaction owns conversations, utterances, interpretations, and original human intent.
- No model/provider call is required for deterministic reminder firing.
- Model output never directly mutates Continuity, Commit, or Capability state.
## Non-goals
- No Gmail, MCP, Codex Bridge, browser automation, mobile app, voice, or self-extension loop.
- No new canonical owner.
- No generic scheduler/event-bus/autonomous-agent loop.
- No new third-party dependency.
- No new notification channel beyond the existing receiver/Windows display path.
## Consequences
- Reminders survive runtime restart because due detection scans canonical `ProspectiveItem` rows.
- Duplicate notification is prevented by Continuity CAS/unique attention source and Commit/receiver idempotency.
- The first attention source is reminder due events; future sources can reuse the same attention path without changing ownership.
- Human UI no longer needs internal UUIDs, binding IDs, provider IDs, or raw lifecycle enums for ordinary use.
+9 -2
View File
@@ -98,7 +98,8 @@
"id": "mod.continuity",
"owns": [
"Commitment",
"ProspectiveItem"
"ProspectiveItem",
"AttentionItem"
],
"database_schema": "continuity",
"boundary_class": "remote_capable",
@@ -114,7 +115,13 @@
"owns": [
"Capability",
"CapabilityBinding",
"CapabilityHealth"
"CapabilityHealth",
"CapabilityImplementation",
"CapabilityResolution",
"CapabilitySelection",
"CapabilityGap",
"AcquisitionStrategyCandidate",
"CapabilityUsageObservation"
],
"database_schema": "capability",
"boundary_class": "remote_capable",
+2 -2
View File
@@ -41,14 +41,14 @@ modules:
continuity:
id: mod.continuity
owns: [Commitment, ProspectiveItem]
owns: [Commitment, ProspectiveItem, AttentionItem]
database_schema: continuity
boundary_class: remote_capable
may_depend_on: [contracts.work, contracts.identity, contracts.authority, platform.events]
capability:
id: mod.capability
owns: [Capability, CapabilityBinding, CapabilityHealth]
owns: [Capability, CapabilityBinding, CapabilityHealth, CapabilityImplementation, CapabilityResolution, CapabilitySelection, CapabilityGap, AcquisitionStrategyCandidate, CapabilityUsageObservation]
database_schema: capability
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, contracts.artifact]
+26
View File
@@ -183,3 +183,29 @@ func (c creatorContinuityClient) Item(ctx context.Context, authorization string,
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
}
func (c creatorContinuityClient) InternalItem(ctx context.Context, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
target := *c.baseURL
target.Path = "/v1/prospective-items/" + url.PathEscape(string(id))
target.RawQuery = ""
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
if err != nil {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
response, err := c.client.Do(request)
if err != nil {
return continuity.ProspectiveItem{}, fmt.Errorf("%w: %v", errCreatorQueryUnavailable, err)
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotFound {
return continuity.ProspectiveItem{}, errCreatorSubjectMissing
}
if response.StatusCode != http.StatusOK {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
var item continuity.ProspectiveItem
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<10)).Decode(&item); err != nil {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
return item, nil
}
+135
View File
@@ -0,0 +1,135 @@
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/notificationexecutor"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/work"
commitdomain "git.cin.su/m/Assistant/modules/commit/domain"
"git.cin.su/m/Assistant/modules/commit/ports"
"git.cin.su/m/Assistant/platform/ids"
)
const prospectiveNotificationActor identity.ActorID = "domovoy-creator-actor"
type internalNotificationContinuity interface {
InternalItem(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error)
}
type internalNotificationHandler struct {
core creatorSessionSource
continuity internalNotificationContinuity
authority creatorAuthoritySource
repository creatorCommitRepository
executor ports.Executor
newID func() (ids.ID, error)
now func() time.Time
}
type internalProspectiveNotificationRequest struct {
ProspectiveItemID continuity.ProspectiveItemID `json:"prospective_item_id"`
ExpectedRevision continuity.ProspectiveRevision `json:"expected_revision"`
}
func registerInternalNotificationRoutes(mux *http.ServeMux, handler internalNotificationHandler, token string) {
mux.HandleFunc("POST /v1/internal/continuity/notify-due", requireCommitToken(token, handler.notifyDue))
}
func (h internalNotificationHandler) notifyDue(w http.ResponseWriter, r *http.Request) {
var request internalProspectiveNotificationRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || ensureCreatorEOF(decoder) != nil || request.ProspectiveItemID == "" || request.ExpectedRevision == 0 {
http.Error(w, "invalid prospective notification request", http.StatusBadRequest)
return
}
item, err := h.continuity.InternalItem(r.Context(), request.ProspectiveItemID)
if errors.Is(err, errCreatorSubjectMissing) {
http.Error(w, "prospective item not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "Continuity item unavailable", http.StatusServiceUnavailable)
return
}
if item.State != continuity.ProspectiveDelivered || item.Revision != request.ExpectedRevision || item.OwnerPrincipalID == "" || item.ScopeID == "" {
http.Error(w, "prospective item not deliverable", http.StatusConflict)
return
}
grant, scope, err := h.authority.LoadGrant(r.Context(), creatorGrantID)
if err != nil || scope.ID != item.ScopeID {
http.Error(w, "Creator authority unavailable", http.StatusConflict)
return
}
identityBinding, err := h.core.Binding(r.Context(), prospectiveNotificationActor, item.OwnerPrincipalID)
if err != nil {
http.Error(w, "Creator identity binding unavailable", http.StatusConflict)
return
}
capabilityBinding, err := h.core.ActiveCapabilityBinding(r.Context(), commitcontract.CapabilityProspectiveNotification, item.OwnerPrincipalID)
if err != nil {
http.Error(w, "notification capability unavailable", http.StatusConflict)
return
}
intentID, authorizedID, attemptID, err := internalActionIDs(h.newID)
if err != nil {
http.Error(w, "action identity unavailable", http.StatusServiceUnavailable)
return
}
title := "Домовой: напоминание"
message := "Пора: " + strings.TrimSpace(item.Summary)
payload := notificationexecutor.Payload{ActionIntentID: intentID, TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, ProspectiveItemID: string(item.ID), ExpectedRevision: int64(item.Revision), Title: title, Message: message}
digest, err := notificationexecutor.Digest(payload)
if err != nil {
http.Error(w, "notification evidence unavailable", http.StatusInternalServerError)
return
}
proposal := commitcontract.ActionProposal{
IntentID: intentID, ActorID: prospectiveNotificationActor, CapabilityID: commitcontract.CapabilityProspectiveNotification,
Target: work.TaskTarget{ScopeID: item.ScopeID, Revision: 1}, Operation: commitcontract.OperationDeliverProspectiveNotification,
NotificationSubject: commitcontract.ProspectiveNotificationSubject{ProspectiveItemID: item.ID, ExpectedRevision: item.Revision, TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, BindingID: capabilityBinding.ID, BindingRevision: capabilityBinding.Revision, RenderedTitle: title, RenderedMessage: message, RequestDigest: digest},
}
action, err := commitdomain.Authorize(proposal, commitdomain.AuthorizationContext{Binding: identityBinding, Scope: scope, Grant: grant}, authorityValidator{}, authorizedID, h.now().UnixMilli())
if err != nil {
http.Error(w, "Creator authorization denied", http.StatusConflict)
return
}
if err = h.repository.SaveAuthorized(r.Context(), action); err != nil {
http.Error(w, "commit authorized action", http.StatusInternalServerError)
return
}
attempt, operationErr := executeAuthorized(r.Context(), h.repository, h.executor, action, attemptID)
if operationErr != nil && !errors.Is(operationErr, commitdomain.ErrUnknownOutcome) {
http.Error(w, "execute committed notification", http.StatusInternalServerError)
return
}
status := http.StatusCreated
if errors.Is(operationErr, commitdomain.ErrUnknownOutcome) {
status = http.StatusAccepted
}
writeCreatorActionResponse(w, status, action, attempt)
}
func internalActionIDs(newID func() (ids.ID, error)) (commitcontract.ActionIntentID, commitcontract.AuthorizedActionID, commitcontract.ActionAttemptID, error) {
intent, err := newID()
if err != nil {
return "", "", "", err
}
authorized, err := newID()
if err != nil {
return "", "", "", err
}
attempt, err := newID()
if err != nil {
return "", "", "", err
}
return commitcontract.ActionIntentID(intent), commitcontract.AuthorizedActionID(authorized), commitcontract.ActionAttemptID(attempt), nil
}
+4
View File
@@ -89,6 +89,10 @@ func main() {
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
executor: dispatcher, reconciler: dispatcher, newID: ids.New, now: time.Now,
})
registerInternalNotificationRoutes(mux, internalNotificationHandler{
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
executor: dispatcher, newID: ids.New, now: time.Now,
}, executorToken)
mux.HandleFunc("POST /v1/authorized-actions", func(w http.ResponseWriter, r *http.Request) {
var request authorizationRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+112
View File
@@ -0,0 +1,112 @@
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
)
type attentionRepository interface {
AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error)
}
func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorIdentityClient, repository attentionRepository) {
mux.HandleFunc("GET /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) {
session, ok := creatorSessionForAttention(w, r, identityClient)
if !ok {
return
}
limit, err := parseListLimit(r.URL.Query().Get("limit"))
if err != nil {
http.Error(w, "invalid attention limit", http.StatusBadRequest)
return
}
items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, limit)
if err != nil {
http.Error(w, "attention unavailable", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(continuity.AttentionListResult{Items: items})
})
mux.HandleFunc("GET /v1/creator/today", func(w http.ResponseWriter, r *http.Request) {
session, ok := creatorSessionForAttention(w, r, identityClient)
if !ok {
return
}
items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, 50)
if err != nil {
http.Error(w, "today unavailable", http.StatusInternalServerError)
return
}
result := continuity.TodayResult{Items: make([]continuity.TodayItem, 0, len(items))}
for _, item := range items {
result.Items = append(result.Items, continuity.TodayItem{
Title: item.Summary,
WhenUnixMillis: item.DueAtUnixMillis,
AttentionLevel: item.Level,
AttentionState: item.State,
HumanState: humanAttentionState(item.State),
DeliveryState: humanDeliveryState(item),
SourceDescription: humanAttentionSource(item.SourceKind),
})
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(result)
})
}
func creatorSessionForAttention(w http.ResponseWriter, r *http.Request, identityClient creatorIdentityClient) (identity.CreatorSession, bool) {
session, err := identityClient.Session(r.Context(), r.Header.Get("Authorization"))
if errors.Is(err, errCreatorSessionDenied) {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return identity.CreatorSession{}, false
}
if err != nil {
http.Error(w, "identity service unavailable", http.StatusServiceUnavailable)
return identity.CreatorSession{}, false
}
return session, true
}
func humanAttentionState(state continuity.AttentionState) string {
switch state {
case continuity.AttentionPending:
return "ожидает внимания"
case continuity.AttentionDelivering:
return "доставляется"
case continuity.AttentionDelivered:
return "доставлено"
case continuity.AttentionSuppressed:
return "скрыто"
case continuity.AttentionExpired:
return "истекло"
case continuity.AttentionFailed:
return "не доставлено"
default:
return "состояние уточняется"
}
}
func humanDeliveryState(item continuity.AttentionItem) string {
if item.State == continuity.AttentionDelivered {
return "уведомление отправлено"
}
if item.State == continuity.AttentionDelivering {
return "уведомление отправляется"
}
return "уведомление ещё не отправлено"
}
func humanAttentionSource(kind continuity.AttentionSourceKind) string {
if kind == continuity.AttentionSourceProspectiveReminder {
return "напоминание"
}
return "событие Домового"
}
+195
View File
@@ -0,0 +1,195 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"time"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/platform/ids"
)
type dueReminderRepository interface {
DueArmed(context.Context, int64, int) ([]continuity.ProspectiveItem, error)
ClaimDueReminder(context.Context, continuity.ProspectiveItemID, continuity.ProspectiveRevision, continuity.AttentionItemID, int64) (continuity.ProspectiveItem, continuity.AttentionItem, error)
PendingAttention(context.Context, int) ([]continuity.AttentionItem, error)
MarkAttentionDelivering(context.Context, continuity.AttentionItemID, continuity.AttentionRevision) (continuity.AttentionItem, error)
MarkAttentionDelivered(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string, int64) (continuity.AttentionItem, error)
MarkAttentionFailed(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string) (continuity.AttentionItem, error)
Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error)
}
type dueNotificationClient struct {
baseURL *url.URL
token string
client *http.Client
}
type dueNotifier interface {
NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error)
}
type dueNotificationRequest struct {
ProspectiveItemID continuity.ProspectiveItemID `json:"prospective_item_id"`
ExpectedRevision continuity.ProspectiveRevision `json:"expected_revision"`
}
type dueNotificationResponse struct {
AuthorizedActionID commitcontract.AuthorizedActionID `json:"AuthorizedActionID"`
ActionIntentID commitcontract.ActionIntentID `json:"ActionIntentID"`
ActionAttemptID commitcontract.ActionAttemptID `json:"ActionAttemptID"`
State commitcontract.ActionState `json:"State"`
}
func newDueNotificationClient(rawURL, token string, client *http.Client) (dueNotificationClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" || token == "" {
return dueNotificationClient{}, fmt.Errorf("invalid Commit due notification configuration")
}
if client == nil {
client = http.DefaultClient
}
return dueNotificationClient{baseURL: base, token: token, client: client}, nil
}
func (c dueNotificationClient) NotifyDue(ctx context.Context, item continuity.ProspectiveItem) (dueNotificationResponse, error) {
target := *c.baseURL
target.Path = "/v1/internal/continuity/notify-due"
payload, err := json.Marshal(dueNotificationRequest{ProspectiveItemID: item.ID, ExpectedRevision: item.Revision})
if err != nil {
return dueNotificationResponse{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return dueNotificationResponse{}, err
}
request.Header.Set("Authorization", "Bearer "+c.token)
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return dueNotificationResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusAccepted {
return dueNotificationResponse{}, fmt.Errorf("commit due notification status %d", response.StatusCode)
}
var result dueNotificationResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return dueNotificationResponse{}, err
}
if result.ActionIntentID == "" || result.AuthorizedActionID == "" || result.ActionAttemptID == "" {
return dueNotificationResponse{}, fmt.Errorf("invalid commit due notification response")
}
return result, nil
}
type dueReminderWorker struct {
repository dueReminderRepository
commit dueNotifier
now func() time.Time
newID func() (ids.ID, error)
interval time.Duration
limit int
}
func (w dueReminderWorker) Start(ctx context.Context) {
if w.interval <= 0 {
w.interval = 15 * time.Second
}
if w.limit <= 0 {
w.limit = 10
}
go func() {
w.process(ctx)
ticker := time.NewTicker(w.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
w.process(ctx)
}
}
}()
}
func (w dueReminderWorker) process(ctx context.Context) {
now := w.now()
if err := w.claimDue(ctx, now); err != nil {
log.Printf("due reminder claim skipped: %v", err)
}
if err := w.deliverPending(ctx, now); err != nil {
log.Printf("due reminder delivery skipped: %v", err)
}
}
func (w dueReminderWorker) claimDue(ctx context.Context, now time.Time) error {
items, err := w.repository.DueArmed(ctx, now.UnixMilli(), w.limit)
if err != nil {
return err
}
for _, item := range items {
id, err := w.newID()
if err != nil {
return err
}
if _, _, err = w.repository.ClaimDueReminder(ctx, item.ID, item.Revision, continuity.AttentionItemID(id), now.UnixMilli()); err != nil {
log.Printf("due reminder %s not claimed: %v", item.ID, err)
}
}
return nil
}
func (w dueReminderWorker) deliverPending(ctx context.Context, now time.Time) error {
items, err := w.repository.PendingAttention(ctx, w.limit)
if err != nil {
return err
}
for _, attention := range items {
if attention.SourceKind != continuity.AttentionSourceProspectiveReminder || attention.SourceProspectiveID == "" {
continue
}
delivering, err := w.repository.MarkAttentionDelivering(ctx, attention.ID, attention.Revision)
if err != nil {
log.Printf("attention %s not marked delivering: %v", attention.ID, err)
continue
}
prospective, err := w.repository.Load(ctx, delivering.SourceProspectiveID)
if err != nil {
log.Printf("attention %s source unavailable: %v", attention.ID, err)
continue
}
result, err := w.commit.NotifyDue(ctx, prospective)
if err != nil {
log.Printf("attention %s notification unavailable: %v", attention.ID, err)
continue
}
if result.State != commitcontract.ActionSucceeded {
_, err = w.repository.MarkAttentionFailed(ctx, delivering.ID, delivering.Revision,
continuity.CommittedActionIntentID(result.ActionIntentID),
continuity.CommittedAuthorizedActionID(result.AuthorizedActionID),
continuity.CommittedActionAttemptID(result.ActionAttemptID),
string(result.State))
if err != nil {
log.Printf("attention %s delivery failure outcome not persisted: %v", attention.ID, err)
}
continue
}
_, err = w.repository.MarkAttentionDelivered(ctx, delivering.ID, delivering.Revision,
continuity.CommittedActionIntentID(result.ActionIntentID),
continuity.CommittedAuthorizedActionID(result.AuthorizedActionID),
continuity.CommittedActionAttemptID(result.ActionAttemptID),
string(result.State), now.UnixMilli())
if err != nil {
log.Printf("attention %s delivery outcome not persisted: %v", attention.ID, err)
}
}
return nil
}
+125
View File
@@ -0,0 +1,125 @@
package main
import (
"context"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/authority"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type memoryDueRepo struct {
item continuity.ProspectiveItem
attention continuity.AttentionItem
}
func (r *memoryDueRepo) DueArmed(context.Context, int64, int) ([]continuity.ProspectiveItem, error) {
if r.item.State == continuity.ProspectiveArmed {
return []continuity.ProspectiveItem{r.item}, nil
}
return nil, nil
}
func (r *memoryDueRepo) ClaimDueReminder(_ context.Context, id continuity.ProspectiveItemID, revision continuity.ProspectiveRevision, attentionID continuity.AttentionItemID, now int64) (continuity.ProspectiveItem, continuity.AttentionItem, error) {
if r.item.ID != id || r.item.Revision != revision || r.item.State != continuity.ProspectiveArmed {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, nil
}
r.item.State = continuity.ProspectiveDelivered
r.item.Revision += 3
r.attention = continuity.AttentionItem{ID: attentionID, OwnerPrincipalID: r.item.OwnerPrincipalID, ScopeID: r.item.ScopeID, SourceKind: continuity.AttentionSourceProspectiveReminder, SourceProspectiveID: r.item.ID, Level: continuity.AttentionDue, State: continuity.AttentionPending, Summary: r.item.Summary, DueAtUnixMillis: r.item.TriggerAtUnixMillis, CreatedAtUnixMillis: now, Revision: 1}
return r.item, r.attention, nil
}
func (r *memoryDueRepo) PendingAttention(context.Context, int) ([]continuity.AttentionItem, error) {
if r.attention.State == continuity.AttentionPending {
return []continuity.AttentionItem{r.attention}, nil
}
return nil, nil
}
func (r *memoryDueRepo) MarkAttentionDelivering(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) {
if r.attention.ID == id && r.attention.Revision == revision {
r.attention.State = continuity.AttentionDelivering
r.attention.Revision++
}
return r.attention, nil
}
func (r *memoryDueRepo) MarkAttentionDelivered(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string, deliveredAt int64) (continuity.AttentionItem, error) {
if r.attention.ID == id && r.attention.Revision == revision {
r.attention.State = continuity.AttentionDelivered
r.attention.Revision++
r.attention.ActionIntentID = intentID
r.attention.AuthorizedActionID = authorizedID
r.attention.ActionAttemptID = attemptID
r.attention.DeliveryOutcome = outcome
r.attention.DeliveredAtUnixMillis = deliveredAt
}
return r.attention, nil
}
func (r *memoryDueRepo) MarkAttentionFailed(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string) (continuity.AttentionItem, error) {
if r.attention.ID == id && r.attention.Revision == revision {
r.attention.State = continuity.AttentionFailed
r.attention.Revision++
r.attention.ActionIntentID = intentID
r.attention.AuthorizedActionID = authorizedID
r.attention.ActionAttemptID = attemptID
r.attention.DeliveryOutcome = outcome
}
return r.attention, nil
}
func (r *memoryDueRepo) Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
return r.item, nil
}
type fakeDueNotifier struct{ calls int }
func (n *fakeDueNotifier) NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error) {
n.calls++
return dueNotificationResponse{ActionIntentID: "intent", AuthorizedActionID: "authorized", ActionAttemptID: "attempt", State: commitcontract.ActionSucceeded}, nil
}
type unknownDueNotifier struct{ calls int }
func (n *unknownDueNotifier) NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error) {
n.calls++
return dueNotificationResponse{ActionIntentID: "intent", AuthorizedActionID: "authorized", ActionAttemptID: "attempt", State: commitcontract.ActionUnknownOutcome}, nil
}
func TestDueReminderProducesAttentionAndNotificationOnce(t *testing.T) {
repo := &memoryDueRepo{item: continuity.ProspectiveItem{ID: "prospective", OwnerPrincipalID: identity.PrincipalID("owner"), ScopeID: authority.ScopeID("scope"), Summary: "проверить уведомление", TriggerAtUnixMillis: 10, State: continuity.ProspectiveArmed, Revision: 1}}
notifier := &fakeDueNotifier{}
now := time.UnixMilli(20)
worker := dueReminderWorker{repository: repo, commit: notifier, now: func() time.Time { return now }, newID: func() (ids.ID, error) { return ids.ID("attention"), nil }, limit: 10}
worker.process(context.Background())
worker.process(context.Background())
if repo.item.State != continuity.ProspectiveDelivered || repo.attention.State != continuity.AttentionDelivered {
t.Fatalf("unexpected states: prospective=%s attention=%s", repo.item.State, repo.attention.State)
}
if notifier.calls != 1 {
t.Fatalf("expected one notification call, got %d", notifier.calls)
}
if repo.attention.ActionIntentID == "" || repo.attention.DeliveredAtUnixMillis == 0 {
t.Fatal("delivery evidence was not persisted")
}
}
func TestDueReminderUnknownOutcomeDoesNotMarkAttentionDelivered(t *testing.T) {
repo := &memoryDueRepo{item: continuity.ProspectiveItem{ID: "prospective", OwnerPrincipalID: identity.PrincipalID("owner"), ScopeID: authority.ScopeID("scope"), Summary: "проверить уведомление", TriggerAtUnixMillis: 10, State: continuity.ProspectiveArmed, Revision: 1}}
notifier := &unknownDueNotifier{}
now := time.UnixMilli(20)
worker := dueReminderWorker{repository: repo, commit: notifier, now: func() time.Time { return now }, newID: func() (ids.ID, error) { return ids.ID("attention"), nil }, limit: 10}
worker.process(context.Background())
if repo.attention.State != continuity.AttentionFailed {
t.Fatalf("unknown outcome must not be delivered, got %s", repo.attention.State)
}
if repo.attention.DeliveryOutcome != string(commitcontract.ActionUnknownOutcome) {
t.Fatalf("missing unknown outcome evidence: %#v", repo.attention)
}
}
+10
View File
@@ -11,6 +11,7 @@ import (
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/platform/ids"
"git.cin.su/m/Assistant/runtime/health"
)
@@ -34,6 +35,13 @@ func main() {
if executorToken == "" {
log.Fatal("COMMIT_EXECUTOR_TOKEN is required")
}
commitClient, err := newDueNotificationClient(envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), executorToken, &http.Client{Timeout: 10 * time.Second})
if err != nil {
log.Fatal(err)
}
workerContext, cancelWorker := context.WithCancel(context.Background())
defer cancelWorker()
dueReminderWorker{repository: repository, commit: commitClient, now: time.Now, newID: ids.New, interval: 5 * time.Second, limit: 10}.Start(workerContext)
registerInternalExecutorRoutes(mux, repository, executorToken)
mux.HandleFunc("POST /v1/prospective-items", func(w http.ResponseWriter, r *http.Request) {
var item continuity.ProspectiveItem
@@ -47,6 +55,7 @@ func main() {
}
w.WriteHeader(http.StatusCreated)
})
registerCreatorReminderRoute(mux, identityClient, repository)
mux.HandleFunc("GET /v1/prospective-items/{id}", func(w http.ResponseWriter, r *http.Request) {
item, err := repository.Load(r.Context(), continuity.ProspectiveItemID(r.PathValue("id")))
if err != nil {
@@ -103,6 +112,7 @@ func main() {
Items []continuity.ProspectiveItem
}{Items: items})
})
registerCreatorAttentionRoutes(mux, identityClient, repository)
log.Printf("continuity-runtime listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"git.cin.su/m/Assistant/contracts/continuity"
continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain"
"git.cin.su/m/Assistant/platform/ids"
)
type reminderRepository interface {
Save(context.Context, continuity.ProspectiveItem) error
}
func registerCreatorReminderRoute(mux *http.ServeMux, identityClient creatorIdentityClient, repository reminderRepository) {
mux.HandleFunc("POST /v1/creator/reminders", func(w http.ResponseWriter, r *http.Request) {
session, err := identityClient.Session(r.Context(), r.Header.Get("Authorization"))
if errors.Is(err, errCreatorSessionDenied) {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
if err != nil {
http.Error(w, "identity service unavailable", http.StatusServiceUnavailable)
return
}
var request continuity.ScheduleReminderRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid reminder", http.StatusBadRequest)
return
}
request.OwnerPrincipalID = session.PrincipalID
id, err := ids.New()
if err != nil {
http.Error(w, "reminder unavailable", http.StatusServiceUnavailable)
return
}
item, err := continuitydomain.BuildReminder(request, continuity.ProspectiveItemID(id))
if err != nil {
http.Error(w, "invalid reminder", http.StatusBadRequest)
return
}
if err := repository.Save(r.Context(), item); err != nil {
http.Error(w, "save reminder", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(continuity.ScheduleReminderResult{Item: item})
})
}
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
)
func TestCreatorReminderEndpointBuildsOwnedProspectiveItem(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer ok" {
t.Fatal("unexpected identity validation")
}
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
}))
defer core.Close()
identityClient, err := newCreatorIdentityClient(core.URL, core.Client())
if err != nil {
t.Fatal(err)
}
repository := &fakeReminderRepository{}
mux := http.NewServeMux()
registerCreatorReminderRoute(mux, identityClient, repository)
request := httptest.NewRequest(http.MethodPost, "/v1/creator/reminders", strings.NewReader(`{"ScopeID":"scope-1","SourceIntentID":"intent-1","SourceUtteranceID":"utterance-1","Summary":"позвонить врачу","TriggerAtUnixMillis":2000,"RequestedAtUnixMillis":1000}`))
request.Header.Set("Authorization", "Bearer ok")
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated || repository.saved.OwnerPrincipalID != "principal-1" || repository.saved.Summary != "позвонить врачу" || repository.saved.State != continuity.ProspectiveArmed {
t.Fatalf("status=%d saved=%#v body=%s", recorder.Code, repository.saved, recorder.Body.String())
}
}
type fakeReminderRepository struct {
saved continuity.ProspectiveItem
}
func (f *fakeReminderRepository) Save(_ context.Context, item continuity.ProspectiveItem) error {
f.saved = item
return nil
}
+214
View File
@@ -0,0 +1,214 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
"git.cin.su/m/Assistant/platform/ids"
)
type continuityReminderClient struct {
baseURL *url.URL
client *http.Client
}
func newContinuityReminderClient(rawURL string, client *http.Client) (continuityReminderClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
return continuityReminderClient{}, fmt.Errorf("invalid Continuity Runtime URL")
}
if client == nil {
client = http.DefaultClient
}
return continuityReminderClient{baseURL: base, client: client}, nil
}
func (c continuityReminderClient) Schedule(ctx context.Context, authorization string, request continuity.ScheduleReminderRequest) (continuity.ScheduleReminderResult, error) {
target := *c.baseURL
target.Path = "/v1/creator/reminders"
payload, err := json.Marshal(request)
if err != nil {
return continuity.ScheduleReminderResult{}, err
}
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return continuity.ScheduleReminderResult{}, err
}
upstream.Header.Set("Content-Type", "application/json")
upstream.Header.Set("Authorization", authorization)
response, err := c.client.Do(upstream)
if err != nil {
return continuity.ScheduleReminderResult{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
return continuity.ScheduleReminderResult{}, fmt.Errorf("continuity reminder status %d", response.StatusCode)
}
var result continuity.ScheduleReminderResult
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return continuity.ScheduleReminderResult{}, err
}
if result.Item.ID == "" || result.Item.State != continuity.ProspectiveArmed {
return continuity.ScheduleReminderResult{}, fmt.Errorf("invalid reminder result")
}
return result, nil
}
func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityRepository, kind capability.CapabilityKind, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (capability.CapabilityResolution, error) {
requirementID, err := ids.New()
if err != nil {
return capability.CapabilityResolution{}, err
}
requirement := capability.CapabilityResolutionRequirement{
RequirementID: capability.RequirementRef(requirementID),
CapabilityKind: kind,
OwnerPrincipalID: session.PrincipalID,
ActorID: session.ActorID,
OriginalIntentID: capability.OriginalIntentRef(intent.ID),
SourceUtteranceID: capability.SourceUtteranceRef(utterance.ID),
RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly},
AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1,
ObservedAtUnixMillis: now.UnixMilli(),
}
candidates, err := repository.LoadCapabilityImplementations(ctx, kind, session.PrincipalID)
if err != nil {
return capability.CapabilityResolution{}, err
}
resolution, err := capabilitydomain.ResolveCapability(requirement, candidates)
if errors.Is(err, capabilitydomain.ErrNoCapabilityImplementation) && resolution.Gap != nil {
if saveErr := repository.SaveCapabilityGap(ctx, *resolution.Gap); saveErr != nil {
return capability.CapabilityResolution{}, saveErr
}
return resolution, err
}
return resolution, err
}
func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) {
id, err := ids.New()
if err != nil {
return
}
latency := now.Sub(started).Milliseconds()
observation := capability.CapabilityUsageObservation{
ID: capability.CapabilityUsageObservationID(id),
ImplementationID: selected.Implementation.ID,
ImplementationRevision: selected.Implementation.Revision,
CapabilityID: selected.Implementation.CapabilityID,
CapabilityKind: selected.Implementation.CapabilityKind,
OwnerPrincipalID: selected.Implementation.OwnerPrincipalID,
OriginalIntentID: requirement.OriginalIntentID,
SourceUtteranceID: requirement.SourceUtteranceID,
Outcome: outcome,
OutcomeRef: outcomeRef,
LatencyMillis: &latency,
ObservedAtUnixMillis: now.UnixMilli(),
}
_ = repository.SaveCapabilityUsageObservation(ctx, observation)
}
func buildReminderRequest(message string, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (continuity.ScheduleReminderRequest, error) {
trigger, err := parseReminderTime(message, now)
if err != nil {
return continuity.ScheduleReminderRequest{}, err
}
summary := reminderSummary(message)
if summary == "" {
summary = strings.TrimSpace(message)
}
return continuity.ScheduleReminderRequest{
OwnerPrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
SourceIntentID: string(intent.ID),
SourceUtteranceID: string(utterance.ID),
Summary: summary,
TriggerAtUnixMillis: trigger.UnixMilli(),
RequestedAtUnixMillis: now.UnixMilli(),
}, nil
}
func parseReminderTime(message string, now time.Time) (time.Time, error) {
lower := strings.ToLower(message)
relative := regexp.MustCompile(`через\s+(\d{1,3})\s*(минуту|минуты|минут|час|часа|часов)`)
if match := relative.FindStringSubmatch(lower); len(match) > 0 {
value, err := strconv.Atoi(match[1])
if err != nil || value < 1 {
return time.Time{}, fmt.Errorf("invalid relative reminder time")
}
unit := match[2]
if strings.HasPrefix(unit, "час") {
if value > 72 {
return time.Time{}, fmt.Errorf("relative reminder time is too far")
}
return now.Add(time.Duration(value) * time.Hour), nil
}
if value > 24*60 {
return time.Time{}, fmt.Errorf("relative reminder time is too far")
}
return now.Add(time.Duration(value) * time.Minute), nil
}
day := now
if strings.Contains(lower, "послезавтра") {
day = now.AddDate(0, 0, 2)
} else if strings.Contains(lower, "завтра") {
day = now.AddDate(0, 0, 1)
}
hour, minute := 9, 0
re := regexp.MustCompile(`(?:в|на)\s+([0-2]?\d)(?::([0-5]\d))?`)
if match := re.FindStringSubmatch(lower); len(match) > 0 {
parsedHour, err := strconv.Atoi(match[1])
if err != nil || parsedHour > 23 {
return time.Time{}, fmt.Errorf("invalid reminder hour")
}
hour = parsedHour
if match[2] != "" {
parsedMinute, err := strconv.Atoi(match[2])
if err != nil {
return time.Time{}, fmt.Errorf("invalid reminder minute")
}
minute = parsedMinute
}
}
trigger := time.Date(day.Year(), day.Month(), day.Day(), hour, minute, 0, 0, now.Location())
if !trigger.After(now) {
trigger = trigger.AddDate(0, 0, 1)
}
return trigger, nil
}
func reminderSummary(message string) string {
clean := strings.TrimSpace(message)
lower := strings.ToLower(clean)
for _, prefix := range []string{"напомни мне", "напомни"} {
if strings.HasPrefix(lower, prefix) {
clean = strings.TrimSpace(clean[len(prefix):])
lower = strings.ToLower(clean)
break
}
}
for _, word := range []string{"послезавтра", "завтра", "сегодня"} {
clean = strings.TrimSpace(strings.ReplaceAll(strings.ToLower(clean), word, ""))
}
relative := regexp.MustCompile(`(?i)через\s+\d{1,3}\s*(минуту|минуты|минут|час|часа|часов)`)
clean = strings.TrimSpace(relative.ReplaceAllString(clean, ""))
re := regexp.MustCompile(`(?i)(в|на)\s+[0-2]?\d(?::[0-5]\d)?`)
clean = strings.TrimSpace(re.ReplaceAllString(clean, ""))
clean = strings.Trim(clean, " .,!?:;")
return clean
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
)
func TestBuildReminderRequestParsesTomorrowAtTen(t *testing.T) {
now := time.Date(2026, 8, 17, 12, 0, 0, 0, time.FixedZone("MSK", 3*60*60))
request, err := buildReminderRequest("Напомни завтра в 10 позвонить врачу.", identity.CreatorSession{PrincipalID: "principal-1"}, interaction.OriginalHumanIntent{ID: "intent-1"}, interaction.HumanUtterance{ID: "utterance-1"}, now)
if err != nil {
t.Fatal(err)
}
trigger := time.UnixMilli(request.TriggerAtUnixMillis).In(now.Location())
if trigger.Year() != 2026 || trigger.Month() != 8 || trigger.Day() != 18 || trigger.Hour() != 10 || trigger.Minute() != 0 {
t.Fatalf("unexpected trigger: %s", trigger)
}
if request.Summary != "позвонить врачу" || request.SourceIntentID != "intent-1" || request.SourceUtteranceID != "utterance-1" {
t.Fatalf("unexpected request: %#v", request)
}
}
func TestBuildReminderRequestParsesRelativeMinutes(t *testing.T) {
now := time.Date(2026, 8, 17, 12, 0, 0, 0, time.FixedZone("MSK", 3*60*60))
request, err := buildReminderRequest("Напомни мне через 3 минуты проверить уведомление.", identity.CreatorSession{PrincipalID: "principal-1"}, interaction.OriginalHumanIntent{ID: "intent-1"}, interaction.HumanUtterance{ID: "utterance-1"}, now)
if err != nil {
t.Fatal(err)
}
trigger := time.UnixMilli(request.TriggerAtUnixMillis).In(now.Location())
if !trigger.Equal(now.Add(3 * time.Minute)) {
t.Fatalf("unexpected trigger: %s", trigger)
}
if request.Summary != "проверить уведомление" {
t.Fatalf("unexpected summary: %q", request.Summary)
}
}
func TestContinuityReminderClientDoesNotAcceptFalseSuccess(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/reminders" || r.Header.Get("Authorization") != "Bearer session" {
t.Fatal("unexpected reminder request")
}
var request continuity.ScheduleReminderRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.Summary != "позвонить врачу" {
t.Fatalf("bad payload: %#v err=%v", request, err)
}
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(continuity.ScheduleReminderResult{Item: continuity.ProspectiveItem{ID: "reminder-1", State: continuity.ProspectiveArmed, Summary: request.Summary}})
}))
defer upstream.Close()
client, err := newContinuityReminderClient(upstream.URL, upstream.Client())
if err != nil {
t.Fatal(err)
}
result, err := client.Schedule(context.Background(), "Bearer session", continuity.ScheduleReminderRequest{Summary: "позвонить врачу"})
if err != nil || result.Item.ID != "reminder-1" {
t.Fatalf("result=%#v err=%v", result, err)
}
}
func TestReminderSummaryRemovesSchedulingWords(t *testing.T) {
if summary := reminderSummary("Напомни завтра в 10 позвонить врачу."); summary != "позвонить врачу" || strings.Contains(summary, "10") {
t.Fatalf("summary = %q", summary)
}
}
+90
View File
@@ -8,6 +8,7 @@ import (
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
@@ -85,6 +86,10 @@ func main() {
log.Fatal(err)
}
defer interactionRepository.Close()
reminderClient, err := newContinuityReminderClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
@@ -268,10 +273,63 @@ func main() {
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
if isCapabilityQuestion(utterance.Content) {
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
if err == nil && len(kinds) > 0 {
planningReply = "Сейчас у меня подключены реальные capabilities: " + humanCapabilityList(kinds) + ". Для рассуждений я использую отдельный Cognitive Gateway/Router, если локальный CANARY доступен. Возможности вроде отправки почты пока будут фиксироваться как capability gap, а не как выполненное действие."
} else {
planningReply = "Я умею записывать диалог и намерения, работать с текущим контекстом, строить планы и обращаться к Cognitive Gateway при доступности локального CANARY. Реальные action-capabilities подключаются через registry; если capability нет, я фиксирую gap."
}
needsCognition = false
}
if helpPath.Reply != "" {
planningReply = helpPath.Reply
needsCognition = false
}
var capabilityStatus capability.CapabilityResolutionStatus
var capabilityKind capability.CapabilityKind
var capabilityGapID capability.CapabilityGapID
var reminder *continuity.ProspectiveItem
if helpPath.Kind == work.HelpPathDirectCapability && len(helpPath.SuggestedCapabilityKinds) > 0 {
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
started := time.Now()
resolution, resolveErr := resolveChatCapability(r.Context(), capabilityRepository, capabilityKind, session, intent, utterance, now)
capabilityStatus = resolution.Status
if resolveErr == nil && len(resolution.Eligible) > 0 {
reminderRequest, reminderErr := buildReminderRequest(utterance.Content, session, intent, utterance, now)
if reminderErr == nil {
result, scheduleErr := reminderClient.Schedule(r.Context(), r.Header.Get("Authorization"), reminderRequest)
finished := time.Now()
if scheduleErr == nil {
reminder = &result.Item
planningReply = fmt.Sprintf("Готово. Я создал напоминание: %s.", result.Item.Summary)
observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageSucceeded, string(result.Item.ID), started, finished)
} else {
planningReply = "Scheduling capability сейчас недоступна: напоминание не создано. Я не буду притворяться, что действие выполнено."
observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageFailed, "continuity-reminder-unavailable", started, finished)
}
} else {
planningReply = "Мне нужно уточнить время напоминания. Например: «Напомни завтра в 10 позвонить врачу»."
capabilityStatus = capability.ResolutionPartiallyAvailable
}
} else if resolution.Gap != nil {
capabilityGapID = resolution.Gap.ID
planningReply = "Сейчас scheduling capability недоступна, поэтому напоминание не создано. Я зафиксировал capability gap."
}
needsCognition = false
}
if helpPath.Kind == work.HelpPathCapabilityGap && len(helpPath.SuggestedCapabilityKinds) > 0 {
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
resolution, resolveErr := resolveChatCapability(r.Context(), capabilityRepository, capabilityKind, session, intent, utterance, now)
capabilityStatus = resolution.Status
if resolveErr == nil {
planningReply = "Capability найдена, но в этом шаге она не исполняется без отдельного разрешённого action path."
} else if resolution.Gap != nil {
capabilityGapID = resolution.Gap.ID
planningReply = "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксировал capability gap, без ложного выполнения."
}
needsCognition = false
}
if helpPath.NeedsDurableWork {
planning, err := workdomain.BuildPlanningResult(planningInput, now)
if err != nil {
@@ -289,6 +347,7 @@ func main() {
response := interaction.ChatMessageResponse{
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
OriginalIntentID: intent.ID,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: needsCognition,
@@ -298,6 +357,10 @@ func main() {
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
CapabilityStatus: capabilityStatus,
CapabilityKind: capabilityKind,
CapabilityGapID: capabilityGapID,
Reminder: reminder,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
@@ -689,3 +752,30 @@ func readCredentialRefSecret(name, refName string) (string, error) {
Path: fileName,
})
}
func envOr(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func isCapabilityQuestion(message string) bool {
lower := strings.ToLower(message)
return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности")
}
func humanCapabilityList(kinds []capability.CapabilityKind) string {
labels := make([]string, 0, len(kinds))
for _, kind := range kinds {
switch kind {
case capability.CapabilityKindScheduleReminder:
labels = append(labels, "создавать напоминания")
case capability.CapabilityKindCognitiveReasoning:
labels = append(labels, "отвечать через когнитивный gateway")
default:
labels = append(labels, strings.ToLower(strings.ReplaceAll(string(kind), "_", " ")))
}
}
return strings.Join(labels, ", ")
}
+76
View File
@@ -9,6 +9,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
)
@@ -93,3 +94,78 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
t.Fatalf("unexpected trusted gateway request: %#v", cognitiveRequest)
}
}
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActDirective, FallbackText: "fallback", PlanningReply: "Готово. Я создал напоминание: позвонить врачу.", CapabilityStatus: capability.ResolutionAvailable, CapabilityKind: capability.CapabilityKindScheduleReminder})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), "http://cognitive")
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Напомни завтра в 10 позвонить врачу."}`))
request.Header.Set("Authorization", "Bearer session-token")
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var response browserChatResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if response.ReplyText != "Готово. Я создал напоминание: позвонить врачу." || response.CapabilityStatus != string(capability.ResolutionAvailable) || response.CapabilityKind != string(capability.CapabilityKindScheduleReminder) {
t.Fatalf("unexpected browser response: %#v", response)
}
}
func TestChatAttentionQuestionUsesContinuityToday(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, FallbackText: "Сейчас в фокусе: УПО."})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
continuityRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v1/creator/today" {
t.Fatalf("unexpected continuity route %s %s", r.Method, r.URL.Path)
}
_ = json.NewEncoder(w).Encode(continuity.TodayResult{Items: []continuity.TodayItem{{Title: "проверить уведомление", HumanState: "доставлено", DeliveryState: "уведомление отправлено"}}})
}))
defer continuityRuntime.Close()
handler, err := newHandler(core.URL, continuityRuntime.URL, "http://commit", core.Client(), "http://cognitive")
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Что сейчас требует моего внимания?"}`))
request.Header.Set("Authorization", "Bearer session-token")
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var response browserChatResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if !strings.Contains(response.ReplyText, "проверить уведомление") || strings.Contains(response.ReplyText, "УПО") {
t.Fatalf("attention reply did not use canonical today: %#v", response)
}
}
+57 -1
View File
@@ -13,6 +13,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/situation"
@@ -151,6 +152,9 @@ type browserChatResponse struct {
ProjectProvenance string `json:"project_provenance"`
Clarification string `json:"clarification"`
WorkPlanned bool `json:"work_planned"`
CapabilityStatus string `json:"capability_status"`
CapabilityKind string `json:"capability_kind"`
ReminderScheduled bool `json:"reminder_scheduled"`
}
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
@@ -320,6 +324,15 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
if saved.PlanningReply != "" {
reply = saved.PlanningReply
}
if asksAttention(browserRequest.Message) {
var today continuity.TodayResult
status, err = g.getTrustedJSON(r, g.continuity, "/v1/creator/today", "", authorization, &today)
if err == nil && status == http.StatusOK {
reply = renderTodayReply(today)
cognition = "not_requested"
saved.NeedsCognition = false
}
}
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
cognition = "unavailable"
requestID, idErr := ids.New()
@@ -340,7 +353,46 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned})
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned, CapabilityStatus: string(saved.CapabilityStatus), CapabilityKind: string(saved.CapabilityKind), ReminderScheduled: saved.Reminder != nil})
}
func asksAttention(message string) bool {
lower := strings.ToLower(message)
return strings.Contains(lower, "требует моего внимания") || strings.Contains(lower, "требует внимания") || strings.Contains(lower, "что сейчас важно")
}
func renderTodayReply(today continuity.TodayResult) string {
if len(today.Items) == 0 {
return "Сейчас ничего срочного не требует внимания."
}
limit := len(today.Items)
if limit > 5 {
limit = 5
}
var builder strings.Builder
builder.WriteString("Сейчас требует внимания:\n")
for index := 0; index < limit; index++ {
item := today.Items[index]
title := strings.TrimSpace(item.Title)
if title == "" {
title = "событие Домового"
}
state := strings.TrimSpace(item.HumanState)
if state == "" {
state = "ожидает внимания"
}
builder.WriteString("- ")
builder.WriteString(title)
builder.WriteString(" — ")
builder.WriteString(state)
if item.DeliveryState != "" {
builder.WriteString(" (")
builder.WriteString(item.DeliveryState)
builder.WriteString(")")
}
builder.WriteString("\n")
}
return strings.TrimSpace(builder.String())
}
func validAdvisoryResponse(response cognitive.GatewayAdvisoryResponse, request cognitive.GatewayAdvisoryRequest) bool {
@@ -491,6 +543,10 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
}
case method == http.MethodGet && path == "/api/prospective-items":
return targetContinuity, "/v1/creator/prospective-items", true
case method == http.MethodGet && path == "/api/attention":
return targetContinuity, "/v1/creator/attention", true
case method == http.MethodGet && path == "/api/today":
return targetContinuity, "/v1/creator/today", true
case method == http.MethodGet && path == "/api/actions":
return targetCommit, "/v1/creator/authorized-actions", true
case method == http.MethodPost && path == "/api/actions/satisfy-delivered":
+24 -1
View File
@@ -29,12 +29,18 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr) auto; }
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr); }
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
#active-focus { margin-left: auto; color: var(--good); }
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
.human-nav { display: flex; align-items: center; gap: 8px; }
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
.operator-link { color: var(--good); }
.surface-panel { min-height: 0; position: relative; }
.active-panel { display: grid; min-height: 0; }
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
.message.user { align-self: flex-end; background: #151e1a; }
@@ -43,8 +49,25 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.composer { position: sticky; bottom: 0; width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 20px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer button { height: 48px; align-self: end; }
.composer button:disabled { cursor: wait; filter: saturate(.3); }
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .1em; }
.surface-heading button { height: 36px; padding: 0 12px; border: 1px solid var(--line); background: transparent; }
.attention-list, .history-list { display: grid; gap: 12px; }
.attention-card, .history-item { border: 1px solid var(--line); background: #0f1613; padding: 16px; }
.attention-card time { color: var(--signal); font: 12px/1 var(--mono); }
.attention-card h2 { margin: 10px 0 8px; font: 400 24px/1.1 var(--serif); }
.attention-card p, .history-item p { margin: 0 0 10px; color: var(--ink); line-height: 1.45; }
.history-item.user { border-color: #34443c; background: #151e1a; }
.history-item strong { display: block; margin-bottom: 8px; color: var(--signal); }
.empty-state { margin: 24px 0 0; color: var(--muted); }
.empty-state.error { color: var(--bad); }
@media (max-width: 640px) {
.composer { grid-template-columns: 1fr; }
.composer button { width: 100%; }
.chat-header { gap: 10px; padding: 0 12px; }
.chat-header span { display: none; }
.human-nav { overflow-x: auto; }
.operator-link, .nav-tab { padding: 0 9px; }
}
+103 -4
View File
@@ -8,9 +8,14 @@ const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const logoutButton = document.querySelector("#logout");
const activeFocus = document.querySelector("#active-focus");
const todayList = document.querySelector("#today-list");
const refreshToday = document.querySelector("#refresh-today");
const historyList = document.querySelector("#history-list");
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
let localHistory = [];
function showChat() {
loginView.hidden = true;
@@ -37,6 +42,8 @@ function appendMessage(kind, text, meta = "") {
}
messages.appendChild(node);
messages.scrollTop = messages.scrollHeight;
localHistory.push({kind, text, meta, at: new Date()});
renderHistory();
}
function updateActiveFocus(value, provenance = "") {
@@ -52,6 +59,91 @@ async function api(path, options = {}) {
return response.json();
}
function humanMeta(result) {
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена.";
if (result.work_planned) return "Я разложил это как работу.";
return "";
}
function renderHistory() {
if (!historyList) return;
historyList.replaceChildren();
const items = localHistory.slice(-30).reverse();
if (!items.length) {
const empty = document.createElement("p");
empty.className = "empty-state";
empty.textContent = "История появится после сообщений в этом окне.";
historyList.appendChild(empty);
return;
}
for (const item of items) {
const row = document.createElement("article");
row.className = `history-item ${item.kind}`;
const title = document.createElement("strong");
title.textContent = item.kind === "user" ? "Вы" : "Домовой";
const body = document.createElement("p");
body.textContent = item.text;
const time = document.createElement("span");
time.textContent = item.at.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
row.append(title, body, time);
historyList.appendChild(row);
}
}
async function loadToday() {
if (!todayList || !token) return;
todayList.replaceChildren();
const loading = document.createElement("p");
loading.className = "empty-state";
loading.textContent = "Смотрю, что требует внимания...";
todayList.appendChild(loading);
try {
const result = await api("/api/today", {method: "GET"});
todayList.replaceChildren();
const items = result.Items || result.items || [];
if (!items.length) {
const empty = document.createElement("p");
empty.className = "empty-state";
empty.textContent = "Сейчас ничего срочного не требует внимания.";
todayList.appendChild(empty);
return;
}
for (const item of items) {
const card = document.createElement("article");
card.className = "attention-card";
const when = document.createElement("time");
const stamp = item.WhenUnixMillis || item.when_unix_millis || 0;
when.textContent = stamp ? new Date(stamp).toLocaleString("ru-RU", {day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit"}) : "сейчас";
const title = document.createElement("h2");
title.textContent = item.Title || item.title || "Событие Домового";
const state = document.createElement("p");
state.textContent = item.HumanState || item.human_state || "ожидает внимания";
const source = document.createElement("small");
source.textContent = [item.SourceDescription || item.source_description, item.DeliveryState || item.delivery_state].filter(Boolean).join(" · ");
card.append(when, title, state, source);
todayList.appendChild(card);
}
} catch {
todayList.replaceChildren();
const error = document.createElement("p");
error.className = "empty-state error";
error.textContent = "Внимание сейчас недоступно. Чат можно продолжать.";
todayList.appendChild(error);
}
}
function activateTab(panelID) {
for (const tab of navTabs) tab.classList.toggle("active", tab.dataset.tab === panelID);
for (const panel of document.querySelectorAll(".surface-panel")) {
const active = panel.id === panelID;
panel.hidden = !active;
panel.classList.toggle("active-panel", active);
}
if (panelID === "today-panel") loadToday();
if (panelID === "history-panel") renderHistory();
}
loginForm.addEventListener("submit", async (event) => {
event.preventDefault();
loginError.textContent = "";
@@ -79,10 +171,11 @@ chatForm.addEventListener("submit", async (event) => {
conversationID = result.conversation_id;
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
updateActiveFocus(result.active_focus, result.project_provenance);
const semanticMeta = [result.speech_act, `cognition=${result.cognition}`];
if (result.work_planned) semanticMeta.push("work=planned");
if (result.clarification) semanticMeta.push(`clarification=${result.clarification}`);
appendMessage("assistant", result.reply_text || "Сообщение записано.", semanticMeta.join("; "));
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
if (/что сейчас требует моего внимания/i.test(text)) {
await loadToday();
activateTab("today-panel");
}
} catch {
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
} finally {
@@ -91,6 +184,12 @@ chatForm.addEventListener("submit", async (event) => {
}
});
for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
}
if (refreshToday) refreshToday.addEventListener("click", loadToday);
logoutButton.addEventListener("click", () => {
token = "";
conversationID = "";
+28 -6
View File
@@ -25,16 +25,38 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<a href="/" aria-label="Консоль создателя">Домовой</a>
<a href="/chat" aria-label="Домовой">Домовой</a>
<nav class="human-nav" aria-label="Разделы Домового">
<button class="nav-tab active" data-tab="chat-panel" type="button">Чат</button>
<button class="nav-tab" data-tab="today-panel" type="button">Сегодня</button>
<button class="nav-tab" data-tab="history-panel" type="button">История</button>
<a href="/" class="operator-link">Оператор</a>
</nav>
<span id="active-focus">фокус: не выбран</span>
<span id="session-state">сессия активна</span>
<button id="logout" type="button" aria-label="Выйти">↗</button>
</header>
<div id="messages" class="messages" aria-live="polite"></div>
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
</form>
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
</form>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
<div class="surface-heading">
<p>Сегодня / внимание</p>
<button id="refresh-today" type="button">Обновить</button>
</div>
<div id="today-list" class="attention-list"></div>
</section>
<section id="history-panel" class="surface-panel history-surface" hidden>
<div class="surface-heading">
<p>История</p>
<span>Последние сообщения текущего окна</span>
</div>
<div id="history-list" class="history-list"></div>
</section>
</section>
</main>
<script src="/assets/chat.js"></script>
+162
View File
@@ -0,0 +1,162 @@
package capability
import (
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type CapabilityImplementationID ids.ID
type CapabilityResolutionID ids.ID
type CapabilityGapID ids.ID
type CapabilityUsageObservationID ids.ID
type RequirementRef string
type OriginalIntentRef string
type SourceUtteranceRef string
type ImplementationKind string
type ImplementationTargetRef string
type AuthorityPolicyRef string
type DataConstraint string
type CapabilityResolutionStatus string
type GapReason string
type AcquisitionStrategyKind string
type UsageOutcomeClass string
const (
ImplementationInternalFunction ImplementationKind = "INTERNAL_FUNCTION"
ImplementationCognitiveBinding ImplementationKind = "COGNITIVE_BINDING"
ImplementationExternalAdapter ImplementationKind = "EXTERNAL_ADAPTER"
ImplementationAgent ImplementationKind = "AGENT"
AuthorityPolicyCreatorDirectiveV1 AuthorityPolicyRef = "creator-directive-v1"
AuthorityPolicyCognitiveGatewayV1 AuthorityPolicyRef = "cognitive-gateway-v1"
DataConstraintLocalOnly DataConstraint = "LOCAL_ONLY"
DataConstraintExternalAllowed DataConstraint = "EXTERNAL_ALLOWED"
DataConstraintNoCredentialForward DataConstraint = "NO_CREDENTIAL_FORWARD"
ResolutionAvailable CapabilityResolutionStatus = "AVAILABLE"
ResolutionPartiallyAvailable CapabilityResolutionStatus = "PARTIALLY_AVAILABLE"
ResolutionUnavailable CapabilityResolutionStatus = "UNAVAILABLE"
ResolutionUnknown CapabilityResolutionStatus = "UNKNOWN"
GapNoImplementation GapReason = "NO_IMPLEMENTATION"
GapUnhealthy GapReason = "UNHEALTHY"
GapInactive GapReason = "INACTIVE"
GapUnqualified GapReason = "UNQUALIFIED"
GapUnauthorized GapReason = "UNAUTHORIZED"
GapIncompatible GapReason = "INCOMPATIBLE"
GapUnknown GapReason = "UNKNOWN"
AcquireUseAlternative AcquisitionStrategyKind = "USE_ALTERNATIVE"
AcquireDiscoverExisting AcquisitionStrategyKind = "DISCOVER_EXISTING"
AcquireConnectExisting AcquisitionStrategyKind = "CONNECT_EXISTING"
AcquireConfigureExisting AcquisitionStrategyKind = "CONFIGURE_EXISTING"
AcquireExternal AcquisitionStrategyKind = "ACQUIRE_EXTERNAL"
AcquireBuildNew AcquisitionStrategyKind = "BUILD_NEW"
AcquireRequestUserInput AcquisitionStrategyKind = "REQUEST_USER_INPUT"
AcquireUnresolvableCurrently AcquisitionStrategyKind = "UNRESOLVABLE_CURRENTLY"
UsageSucceeded UsageOutcomeClass = "SUCCEEDED"
UsageFailed UsageOutcomeClass = "FAILED"
UsageSkipped UsageOutcomeClass = "SKIPPED"
UsageUnknown UsageOutcomeClass = "UNKNOWN"
)
// CapabilityDescriptor names what Domovoy can obtain or do. CapabilityKind is
// intentionally extensible string semantics, not a closed-world enum.
type CapabilityDescriptor struct {
ID CapabilityID
Kind CapabilityKind
DisplayName string
Description string
Revision int64
RecordedAtUnixMillis int64
}
// CapabilityImplementation is the generic implementation/binding layer. It
// may point at a cognitive binding, internal function, agent, adapter or later
// provider type. CredentialRef is metadata only and never grants authority.
type CapabilityImplementation struct {
ID CapabilityImplementationID
CapabilityID CapabilityID
CapabilityKind CapabilityKind
OwnerPrincipalID identity.PrincipalID
Revision int64
Kind ImplementationKind
TargetRef ImplementationTargetRef
CredentialRef CredentialRef
Qualification QualificationState
Activation ActivationState
Health HealthState
AuthorityPolicyRef AuthorityPolicyRef
DataConstraints []DataConstraint
CostScoreMillis *int64
LatencyScoreMillis *int64
ReliabilityMillis *int64
ObservedAtUnixMillis int64
}
type CapabilityResolutionRequirement struct {
RequirementID RequirementRef
CapabilityKind CapabilityKind
CapabilityID CapabilityID
OwnerPrincipalID identity.PrincipalID
ActorID identity.ActorID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
RequiredConstraints []DataConstraint
AllowCanary bool
AuthorityContextRef AuthorityPolicyRef
ObservedAtUnixMillis int64
}
type ResolvedCapabilityImplementation struct {
Implementation CapabilityImplementation
Selected bool
Rank int
}
type CapabilityResolution struct {
ID CapabilityResolutionID
Requirement CapabilityResolutionRequirement
Status CapabilityResolutionStatus
Eligible []ResolvedCapabilityImplementation
ExcludedReasons map[CapabilityImplementationID]GapReason
Gap *CapabilityGap
ObservedAtUnixMillis int64
}
type CapabilityGap struct {
ID CapabilityGapID
RequirementID RequirementRef
CapabilityKind CapabilityKind
CapabilityID CapabilityID
OwnerPrincipalID identity.PrincipalID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Reason GapReason
Detail string
Strategies []AcquisitionStrategyCandidate
RecordedAtUnixMillis int64
}
type AcquisitionStrategyCandidate struct {
Kind AcquisitionStrategyKind
Reason string
}
type CapabilityUsageObservation struct {
ID CapabilityUsageObservationID
ImplementationID CapabilityImplementationID
ImplementationRevision int64
CapabilityID CapabilityID
CapabilityKind CapabilityKind
OwnerPrincipalID identity.PrincipalID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Outcome UsageOutcomeClass
OutcomeRef string
LatencyMillis *int64
ObservedAtUnixMillis int64
}
+88
View File
@@ -9,9 +9,12 @@ import (
type CommitmentID ids.ID
type ProspectiveItemID ids.ID
type AttentionItemID ids.ID
type ProspectiveRevision uint64
type AttentionRevision uint64
type CommittedActionIntentID ids.ID
type CommittedAuthorizedActionID ids.ID
type CommittedActionAttemptID ids.ID
type ProspectiveState string
@@ -26,6 +29,34 @@ const (
ProspectiveExpired ProspectiveState = "EXPIRED"
)
type AttentionLevel string
const (
AttentionUrgent AttentionLevel = "URGENT"
AttentionActionRequired AttentionLevel = "ACTION_REQUIRED"
AttentionDue AttentionLevel = "DUE"
AttentionImportantChange AttentionLevel = "IMPORTANT_CHANGE"
AttentionFYI AttentionLevel = "FYI"
AttentionBackground AttentionLevel = "BACKGROUND"
)
type AttentionState string
const (
AttentionPending AttentionState = "PENDING"
AttentionDelivering AttentionState = "DELIVERING"
AttentionDelivered AttentionState = "DELIVERED"
AttentionSuppressed AttentionState = "SUPPRESSED"
AttentionExpired AttentionState = "EXPIRED"
AttentionFailed AttentionState = "FAILED"
)
type AttentionSourceKind string
const (
AttentionSourceProspectiveReminder AttentionSourceKind = "PROSPECTIVE_REMINDER"
)
// ProspectiveItem is canonical future relevance. TriggerAtUnixMillis is an
// absolute instant; scheduling engines are derived delivery mechanisms only.
type ProspectiveItem struct {
@@ -34,11 +65,68 @@ type ProspectiveItem struct {
OwnerPrincipalID identity.PrincipalID
ScopeID authority.ScopeID
GoalID work.GoalID
SourceIntentID string
SourceUtteranceID string
Summary string
TriggerAtUnixMillis int64
State ProspectiveState
Revision ProspectiveRevision
}
type ScheduleReminderRequest struct {
OwnerPrincipalID identity.PrincipalID
ScopeID authority.ScopeID
SourceIntentID string
SourceUtteranceID string
Summary string
TriggerAtUnixMillis int64
RequestedAtUnixMillis int64
}
type ScheduleReminderResult struct {
Item ProspectiveItem
}
// AttentionItem is Continuity-owned human-facing relevance state. It can be
// rendered by UIs, but it is not a notification receipt and does not itself
// grant authority for external mutation.
type AttentionItem struct {
ID AttentionItemID
OwnerPrincipalID identity.PrincipalID
ScopeID authority.ScopeID
SourceKind AttentionSourceKind
SourceProspectiveID ProspectiveItemID
Level AttentionLevel
State AttentionState
Summary string
DueAtUnixMillis int64
CreatedAtUnixMillis int64
DeliveredAtUnixMillis int64
ActionIntentID CommittedActionIntentID
AuthorizedActionID CommittedAuthorizedActionID
ActionAttemptID CommittedActionAttemptID
DeliveryOutcome string
Revision AttentionRevision
}
type AttentionListResult struct {
Items []AttentionItem
}
type TodayItem struct {
Title string
WhenUnixMillis int64
AttentionLevel AttentionLevel
AttentionState AttentionState
HumanState string
DeliveryState string
SourceDescription string
}
type TodayResult struct {
Items []TodayItem
}
// SatisfyDeliveredSubject is the complete serializable operation subject. It
// is distinct from a Work TaskTarget, which remains the live justification.
type SatisfyDeliveredSubject struct {
+7
View File
@@ -1,6 +1,8 @@
package interaction
import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/situation"
)
@@ -121,6 +123,7 @@ type ChatMessageRequest struct {
type ChatMessageResponse struct {
ConversationID ConversationID
UtteranceID HumanUtteranceID
OriginalIntentID OriginalHumanIntentID
SpeechAct SpeechAct
InterpretationState InterpretationState
NeedsCognition bool
@@ -130,4 +133,8 @@ type ChatMessageResponse struct {
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
CapabilityStatus capability.CapabilityResolutionStatus
CapabilityKind capability.CapabilityKind
CapabilityGapID capability.CapabilityGapID
Reminder *continuity.ProspectiveItem
}
@@ -407,6 +407,145 @@ CREATE TABLE IF NOT EXISTS capability.cognitive_performance_evidence (
CHECK (latency_micros IS NULL OR latency_micros >= 0), CHECK (memory_bytes IS NULL OR memory_bytes >= 0),
CHECK (reliability_millis IS NULL OR reliability_millis >= 0), CHECK (observed_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_descriptors (
capability_id TEXT PRIMARY KEY,
capability_kind TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT NOT NULL,
revision BIGINT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (capability_id <> ''),
CHECK (capability_kind <> ''),
CHECK (display_name <> ''),
CHECK (revision > 0),
CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_implementations (
implementation_id TEXT NOT NULL,
capability_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
revision BIGINT NOT NULL,
implementation_kind TEXT NOT NULL,
target_ref TEXT NOT NULL,
credential_ref TEXT NOT NULL DEFAULT '',
qualification TEXT NOT NULL,
activation TEXT NOT NULL,
health TEXT NOT NULL,
authority_policy_ref TEXT NOT NULL,
data_constraints TEXT[] NOT NULL,
cost_score_millis BIGINT,
latency_score_millis BIGINT,
reliability_millis BIGINT,
observed_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (implementation_id, revision),
CHECK (implementation_id <> ''),
CHECK (capability_id <> ''),
CHECK (capability_kind <> ''),
CHECK (owner_principal_id <> ''),
CHECK (revision > 0),
CHECK (implementation_kind IN ('INTERNAL_FUNCTION','COGNITIVE_BINDING','EXTERNAL_ADAPTER','AGENT')),
CHECK (target_ref <> ''),
CHECK (qualification IN ('DISCOVERED','PROBING','EVALUATING','VALIDATED','REJECTED')),
CHECK (activation IN ('INACTIVE','CANARY','ACTIVE','DEPRECATED','RETIRED')),
CHECK (health IN ('UNKNOWN','AVAILABLE','DEGRADED','UNAVAILABLE')),
CHECK (authority_policy_ref <> ''),
CHECK (cardinality(data_constraints) > 0),
CHECK (cost_score_millis IS NULL OR cost_score_millis >= 0),
CHECK (latency_score_millis IS NULL OR latency_score_millis >= 0),
CHECK (reliability_millis IS NULL OR reliability_millis >= 0),
CHECK (observed_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_implementation_heads (
implementation_id TEXT PRIMARY KEY,
revision BIGINT NOT NULL,
capability_kind TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_gaps (
gap_id TEXT PRIMARY KEY,
requirement_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
capability_id TEXT NOT NULL DEFAULT '',
owner_principal_id TEXT NOT NULL,
original_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
reason TEXT NOT NULL,
detail TEXT NOT NULL,
strategies TEXT[] NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (requirement_id <> ''),
CHECK (capability_kind <> ''),
CHECK (owner_principal_id <> ''),
CHECK (original_intent_id <> ''),
CHECK (source_utterance_id <> ''),
CHECK (reason IN ('NO_IMPLEMENTATION','UNHEALTHY','INACTIVE','UNQUALIFIED','UNAUTHORIZED','INCOMPATIBLE','UNKNOWN')),
CHECK (detail <> ''),
CHECK (cardinality(strategies) > 0),
CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_usage_observations (
observation_id TEXT PRIMARY KEY,
implementation_id TEXT NOT NULL,
implementation_revision BIGINT NOT NULL,
capability_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
original_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
outcome TEXT NOT NULL,
outcome_ref TEXT NOT NULL DEFAULT '',
latency_millis BIGINT,
observed_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (implementation_id <> ''),
CHECK (implementation_revision > 0),
CHECK (capability_id <> ''),
CHECK (capability_kind <> ''),
CHECK (owner_principal_id <> ''),
CHECK (original_intent_id <> ''),
CHECK (source_utterance_id <> ''),
CHECK (outcome IN ('SUCCEEDED','FAILED','SKIPPED','UNKNOWN')),
CHECK (latency_millis IS NULL OR latency_millis >= 0),
CHECK (observed_at_unix_millis > 0)
);
INSERT INTO capability.capability_descriptors(capability_id, capability_kind, display_name, description, revision, recorded_at_unix_millis)
VALUES
('schedule-reminder', 'SCHEDULE_REMINDER', 'Reminder scheduling', 'Create a Continuity-owned prospective reminder item from an authenticated human directive.', 1, 1),
('creator-advisory', 'COGNITIVE_REASONING', 'Creator advisory cognition', 'Reasoning capability resolved through the specialized Cognitive Registry and Gateway.', 1, 1)
ON CONFLICT (capability_id) DO NOTHING;
INSERT INTO capability.capability_implementations(
implementation_id, capability_id, capability_kind, owner_principal_id, revision, implementation_kind, target_ref,
credential_ref, qualification, activation, health, authority_policy_ref, data_constraints,
cost_score_millis, latency_score_millis, reliability_millis, observed_at_unix_millis)
VALUES (
'internal-reminder-scheduler-v1', 'schedule-reminder', 'SCHEDULE_REMINDER', 'domovoy-creator-principal',
1, 'INTERNAL_FUNCTION', 'continuity://prospective-reminder-v1', '', 'VALIDATED', 'ACTIVE', 'AVAILABLE',
'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 0, 10, 950, 1
)
ON CONFLICT DO NOTHING;
INSERT INTO capability.capability_implementation_heads(implementation_id, revision, capability_kind, owner_principal_id)
VALUES ('internal-reminder-scheduler-v1', 1, 'SCHEDULE_REMINDER', 'domovoy-creator-principal')
ON CONFLICT (implementation_id) DO NOTHING;
INSERT INTO capability.notification_bindings (
binding_id, capability_id, owner_principal_id, revision, state, https_origin,
delivery_path, receipt_path, protocol, secret_ref, verified_at_unix_millis, health
)
VALUES (
'domovoy-notification-binding', 'prospective-notification-delivery', 'domovoy-creator-principal',
2, 'ACTIVE', 'https://assistant.cin.su', '/delivery/v1/deliveries', '/delivery/v1/receipts',
'IDEMPOTENT_RECEIPT_V1', 'domovoy-notification-write-v1', 1, 'AVAILABLE'
)
ON CONFLICT DO NOTHING;
INSERT INTO capability.notification_binding_heads(capability_id, owner_principal_id, binding_id, revision)
VALUES ('prospective-notification-delivery', 'domovoy-creator-principal', 'domovoy-notification-binding', 2)
ON CONFLICT (capability_id, owner_principal_id) DO UPDATE SET binding_id=EXCLUDED.binding_id, revision=EXCLUDED.revision
WHERE capability.notification_binding_heads.revision < EXCLUDED.revision;
RESET ROLE;
SET ROLE continuity_role;
@@ -416,6 +555,9 @@ CREATE TABLE IF NOT EXISTS continuity.prospective_items (
owner_principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
goal_id TEXT NOT NULL,
source_intent_id TEXT NOT NULL DEFAULT '',
source_utterance_id TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
trigger_at_unix_millis BIGINT NOT NULL,
state TEXT NOT NULL,
revision BIGINT NOT NULL,
@@ -436,6 +578,34 @@ CREATE TABLE IF NOT EXISTS continuity.action_receipts (
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (expected_revision > 0), CHECK (result_revision > expected_revision)
);
CREATE TABLE IF NOT EXISTS continuity.attention_items (
attention_item_id TEXT PRIMARY KEY,
owner_principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
source_kind TEXT NOT NULL,
source_prospective_item_id TEXT NOT NULL,
level TEXT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
due_at_unix_millis BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
delivered_at_unix_millis BIGINT NOT NULL DEFAULT 0,
action_intent_id TEXT NOT NULL DEFAULT '',
authorized_action_id TEXT NOT NULL DEFAULT '',
action_attempt_id TEXT NOT NULL DEFAULT '',
delivery_outcome TEXT NOT NULL DEFAULT '',
revision BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (source_kind, source_prospective_item_id),
CHECK (source_kind IN ('PROSPECTIVE_REMINDER')),
CHECK (level IN ('URGENT','ACTION_REQUIRED','DUE','IMPORTANT_CHANGE','FYI','BACKGROUND')),
CHECK (state IN ('PENDING','DELIVERING','DELIVERED','SUPPRESSED','EXPIRED','FAILED')),
CHECK (summary <> ''),
CHECK (due_at_unix_millis > 0),
CHECK (created_at_unix_millis > 0),
CHECK (revision > 0),
CHECK (state <> 'DELIVERED' OR (delivered_at_unix_millis > 0 AND action_intent_id <> '' AND authorized_action_id <> '' AND action_attempt_id <> '' AND delivery_outcome <> ''))
);
RESET ROLE;
SET ROLE interaction_role;
@@ -0,0 +1,139 @@
BEGIN;
SET ROLE capability_role;
CREATE TABLE IF NOT EXISTS capability.capability_descriptors (
capability_id TEXT PRIMARY KEY,
capability_kind TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT NOT NULL,
revision BIGINT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (capability_id <> ''),
CHECK (capability_kind <> ''),
CHECK (display_name <> ''),
CHECK (revision > 0),
CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_implementations (
implementation_id TEXT NOT NULL,
capability_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
revision BIGINT NOT NULL,
implementation_kind TEXT NOT NULL,
target_ref TEXT NOT NULL,
credential_ref TEXT NOT NULL DEFAULT '',
qualification TEXT NOT NULL,
activation TEXT NOT NULL,
health TEXT NOT NULL,
authority_policy_ref TEXT NOT NULL,
data_constraints TEXT[] NOT NULL,
cost_score_millis BIGINT,
latency_score_millis BIGINT,
reliability_millis BIGINT,
observed_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (implementation_id, revision),
CHECK (implementation_id <> ''),
CHECK (capability_id <> ''),
CHECK (capability_kind <> ''),
CHECK (owner_principal_id <> ''),
CHECK (revision > 0),
CHECK (implementation_kind IN ('INTERNAL_FUNCTION','COGNITIVE_BINDING','EXTERNAL_ADAPTER','AGENT')),
CHECK (target_ref <> ''),
CHECK (qualification IN ('DISCOVERED','PROBING','EVALUATING','VALIDATED','REJECTED')),
CHECK (activation IN ('INACTIVE','CANARY','ACTIVE','DEPRECATED','RETIRED')),
CHECK (health IN ('UNKNOWN','AVAILABLE','DEGRADED','UNAVAILABLE')),
CHECK (authority_policy_ref <> ''),
CHECK (cardinality(data_constraints) > 0),
CHECK (cost_score_millis IS NULL OR cost_score_millis >= 0),
CHECK (latency_score_millis IS NULL OR latency_score_millis >= 0),
CHECK (reliability_millis IS NULL OR reliability_millis >= 0),
CHECK (observed_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_implementation_heads (
implementation_id TEXT PRIMARY KEY,
revision BIGINT NOT NULL,
capability_kind TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_gaps (
gap_id TEXT PRIMARY KEY,
requirement_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
capability_id TEXT NOT NULL DEFAULT '',
owner_principal_id TEXT NOT NULL,
original_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
reason TEXT NOT NULL,
detail TEXT NOT NULL,
strategies TEXT[] NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (requirement_id <> ''),
CHECK (capability_kind <> ''),
CHECK (owner_principal_id <> ''),
CHECK (original_intent_id <> ''),
CHECK (source_utterance_id <> ''),
CHECK (reason IN ('NO_IMPLEMENTATION','UNHEALTHY','INACTIVE','UNQUALIFIED','UNAUTHORIZED','INCOMPATIBLE','UNKNOWN')),
CHECK (detail <> ''),
CHECK (cardinality(strategies) > 0),
CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.capability_usage_observations (
observation_id TEXT PRIMARY KEY,
implementation_id TEXT NOT NULL,
implementation_revision BIGINT NOT NULL,
capability_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
original_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
outcome TEXT NOT NULL,
outcome_ref TEXT NOT NULL DEFAULT '',
latency_millis BIGINT,
observed_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (implementation_id <> ''),
CHECK (implementation_revision > 0),
CHECK (capability_id <> ''),
CHECK (capability_kind <> ''),
CHECK (owner_principal_id <> ''),
CHECK (original_intent_id <> ''),
CHECK (source_utterance_id <> ''),
CHECK (outcome IN ('SUCCEEDED','FAILED','SKIPPED','UNKNOWN')),
CHECK (latency_millis IS NULL OR latency_millis >= 0),
CHECK (observed_at_unix_millis > 0)
);
INSERT INTO capability.capability_descriptors(capability_id, capability_kind, display_name, description, revision, recorded_at_unix_millis)
VALUES
('schedule-reminder', 'SCHEDULE_REMINDER', 'Reminder scheduling', 'Create a Continuity-owned prospective reminder item from an authenticated human directive.', 1, 1),
('creator-advisory', 'COGNITIVE_REASONING', 'Creator advisory cognition', 'Reasoning capability resolved through the specialized Cognitive Registry and Gateway.', 1, 1)
ON CONFLICT (capability_id) DO NOTHING;
INSERT INTO capability.capability_implementations(
implementation_id, capability_id, capability_kind, owner_principal_id, revision, implementation_kind, target_ref,
credential_ref, qualification, activation, health, authority_policy_ref, data_constraints,
cost_score_millis, latency_score_millis, reliability_millis, observed_at_unix_millis)
VALUES (
'internal-reminder-scheduler-v1', 'schedule-reminder', 'SCHEDULE_REMINDER', 'domovoy-creator-principal',
1, 'INTERNAL_FUNCTION', 'continuity://prospective-reminder-v1', '', 'VALIDATED', 'ACTIVE', 'AVAILABLE',
'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 0, 10, 950, 1
)
ON CONFLICT DO NOTHING;
INSERT INTO capability.capability_implementation_heads(implementation_id, revision, capability_kind, owner_principal_id)
VALUES ('internal-reminder-scheduler-v1', 1, 'SCHEDULE_REMINDER', 'domovoy-creator-principal')
ON CONFLICT (implementation_id) DO NOTHING;
RESET ROLE;
COMMIT;
@@ -0,0 +1,10 @@
BEGIN;
SET ROLE continuity_role;
ALTER TABLE continuity.prospective_items ADD COLUMN IF NOT EXISTS source_intent_id TEXT NOT NULL DEFAULT '';
ALTER TABLE continuity.prospective_items ADD COLUMN IF NOT EXISTS source_utterance_id TEXT NOT NULL DEFAULT '';
ALTER TABLE continuity.prospective_items ADD COLUMN IF NOT EXISTS summary TEXT NOT NULL DEFAULT '';
RESET ROLE;
COMMIT;
@@ -0,0 +1,35 @@
BEGIN;
SET ROLE continuity_role;
CREATE TABLE IF NOT EXISTS continuity.attention_items (
attention_item_id TEXT PRIMARY KEY,
owner_principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
source_kind TEXT NOT NULL,
source_prospective_item_id TEXT NOT NULL,
level TEXT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
due_at_unix_millis BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
delivered_at_unix_millis BIGINT NOT NULL DEFAULT 0,
action_intent_id TEXT NOT NULL DEFAULT '',
authorized_action_id TEXT NOT NULL DEFAULT '',
action_attempt_id TEXT NOT NULL DEFAULT '',
delivery_outcome TEXT NOT NULL DEFAULT '',
revision BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (source_kind, source_prospective_item_id),
CHECK (source_kind IN ('PROSPECTIVE_REMINDER')),
CHECK (level IN ('URGENT','ACTION_REQUIRED','DUE','IMPORTANT_CHANGE','FYI','BACKGROUND')),
CHECK (state IN ('PENDING','DELIVERING','DELIVERED','SUPPRESSED','EXPIRED','FAILED')),
CHECK (summary <> ''),
CHECK (due_at_unix_millis > 0),
CHECK (created_at_unix_millis > 0),
CHECK (revision > 0),
CHECK (state <> 'DELIVERED' OR (delivered_at_unix_millis > 0 AND action_intent_id <> '' AND authorized_action_id <> '' AND action_attempt_id <> '' AND delivery_outcome <> ''))
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,43 @@
BEGIN;
SET ROLE capability_role;
INSERT INTO capability.notification_bindings (
binding_id, capability_id, owner_principal_id, revision, state, https_origin,
delivery_path, receipt_path, protocol, secret_ref, verified_at_unix_millis, health
)
VALUES (
'domovoy-notification-binding',
'prospective-notification-delivery',
'domovoy-creator-principal',
2,
'ACTIVE',
'https://assistant.cin.su',
'/delivery/v1/deliveries',
'/delivery/v1/receipts',
'IDEMPOTENT_RECEIPT_V1',
'domovoy-notification-write-v1',
FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
'AVAILABLE'
)
ON CONFLICT (binding_id, revision) DO UPDATE SET
https_origin = EXCLUDED.https_origin,
verified_at_unix_millis = EXCLUDED.verified_at_unix_millis,
health = EXCLUDED.health;
INSERT INTO capability.notification_binding_heads (
capability_id, owner_principal_id, binding_id, revision
)
VALUES (
'prospective-notification-delivery',
'domovoy-creator-principal',
'domovoy-notification-binding',
2
)
ON CONFLICT (capability_id, owner_principal_id) DO UPDATE SET
binding_id = EXCLUDED.binding_id,
revision = EXCLUDED.revision
WHERE capability.notification_binding_heads.revision < EXCLUDED.revision;
RESET ROLE;
COMMIT;
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000063-intent-work-planning-foundation.json",
"task_id": "DEV-000063",
"task_file": "dev/tasks/DEV-000065-assistant-presence-attention-human-surface.json",
"task_id": "DEV-000065",
"task_revision": 1
}
@@ -0,0 +1,161 @@
{
"task_id": "DEV-000064",
"task_revision": 1,
"architecture_revision": "ADR-0013",
"base_repo_revision": "fe531ab56f43bb37a5786e38b094781d3eccb775",
"risk_tier": "E",
"intent": "Add provider-neutral capability implementation resolution, selection and typed gap handling foundation, with reminder scheduling as the first production end-to-end capability.",
"requirement_ids": [
"ADR-0013",
"INV-AUTH-001",
"INV-EPI-001",
"INV-OWN-001",
"Capability availability is not Authority",
"Cognitive Registry remains the specialized cognitive capability registry"
],
"allowed_change_scope": {
"modules": [
"capability",
"continuity",
"interaction",
"work"
],
"paths": [
"architecture/adrs/ADR-0013-capability-resolution-selection-gap-foundation.md",
"architecture/architecture-registry.yaml",
"architecture/architecture-registry.json",
"contracts/capability/**",
"contracts/continuity/**",
"contracts/interaction/**",
"contracts/work/**",
"modules/capability/**",
"modules/continuity/**",
"modules/work/**",
"adapters/postgres/**",
"database/bootstrap/**",
"database/migrations/**",
"cmd/core-runtime/**",
"cmd/continuity-runtime/**",
"cmd/creator-web/**",
"tests/capability_resolution/**",
"tests/owner_acl/**",
"tests/cognitive_advisory/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000064-capability-resolution-selection-gap-foundation.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/ARCHITECTURE_CONSTITUTION.md",
"modules/authority/**",
"modules/commit/**",
"cmd/commit-runtime/**",
"security/**",
"ci/**",
".gitea/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 3,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0
},
"contracts": {
"consume": [
"Work CapabilityRequirement",
"Interaction OriginalHumanIntent",
"Continuity ProspectiveItem",
"Cognitive Registry / Router",
"Secret Resolver CredentialRef",
"Authority boundary concepts"
],
"may_change": [
"contracts/capability@v1",
"contracts/continuity@v1",
"contracts/interaction@v1"
]
},
"semantic_owner": "capability",
"canonical_state": [
"Capability",
"CapabilityImplementation",
"CapabilityResolution",
"CapabilitySelection",
"CapabilityGap",
"AcquisitionStrategyCandidate",
"CapabilityUsageObservation"
],
"state_transitions": [
"implementation qualification DISCOVERED/PROBING/EVALUATING/VALIDATED/REJECTED",
"implementation activation INACTIVE/CANARY/ACTIVE/DEPRECATED/RETIRED",
"implementation health UNKNOWN/AVAILABLE/DEGRADED/UNAVAILABLE",
"resolution AVAILABLE/PARTIALLY_AVAILABLE/UNAVAILABLE/UNKNOWN",
"gap records reasons without executing or granting Authority"
],
"side_effects_and_privileges": [
"Capability registry writes are owner-local",
"Reminder scheduling creates Continuity-owned ProspectiveItem through Continuity runtime boundary",
"No external mutation",
"No Commit/Executor invocation for reminder registration",
"No ACTIVE cognitive binding promotion",
"No OpenAI activation",
"CredentialRef is metadata and does not grant Authority"
],
"persistence_and_migrations": [
"capability owner-local implementation registry",
"capability owner-local gap records",
"capability owner-local generic usage observations",
"continuity owner-local reminder creation endpoint using existing prospective_items",
"no cross-owner FK",
"no cross-owner SQL join dependency"
],
"failure_retry_restart": [
"registered implementation can be unavailable without breaking startup",
"resolver fails closed when implementation is missing, unhealthy, inactive, unqualified or unauthorized",
"capability gap is persisted with OriginalHumanIntent correlation",
"reminder creation is persisted as a Continuity ProspectiveItem",
"cognitive router remains authoritative for cognitive invocation"
],
"assumptions": [
"DEV-000063 is production baseline",
"Capability owner is canonical Capability registry owner per Core Semantic Model",
"Continuity owner remains canonical ProspectiveItem owner",
"existing CPU CANARY remains not ACTIVE",
"docker-test prohibited"
],
"acceptance_scenarios": [
"Reminder resolves to scheduling implementation",
"Reminder is actually created and can be verified",
"Simple question still works without durable Work",
"Unsupported SEND_EMAIL returns persisted CapabilityGap",
"Unavailable implementation is excluded",
"Same capability with two implementations can be represented",
"Selection does not depend on hardcoded provider name",
"CredentialRef does not grant Authority",
"Capability is not Authority",
"Capability gap preserves OriginalHumanIntent correlation",
"One Goal can require multiple capabilities",
"Cognitive binding remains functioning",
"RTX unavailable does not break CPU cognition",
"LLM-free mode passes"
],
"required_tests": [
"go test ./...",
"go vet ./...",
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"go run ./cmd/devctl check-architecture",
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
"production live /chat acceptance"
],
"terminal_gap_states": [
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT"
]
}
@@ -0,0 +1,152 @@
{
"task_id": "DEV-000065",
"task_revision": 1,
"architecture_revision": "ADR-0014",
"base_repo_revision": "6e73318e49991b40a5ed750c604792cbd95bd325",
"risk_tier": "E",
"intent": "Add assistant presence through Continuity-owned due reminder processing, typed Attention foundation, governed outbound notification delivery, and a human-facing Assistant surface with Chat, Today/Attention, and History while preserving the Operator Console.",
"requirement_ids": [
"ADR-0014",
"ADR-0005",
"INV-AUTH-001",
"INV-OWN-001",
"INV-CONT-001",
"INV-EPI-001",
"Capability availability is not Authority",
"Durable future obligations survive runtime restart"
],
"allowed_change_scope": {
"modules": [
"continuity",
"commit",
"capability",
"interaction"
],
"paths": [
"architecture/adrs/ADR-0014-assistant-presence-attention-human-surface.md",
"architecture/architecture-registry.yaml",
"architecture/architecture-registry.json",
"contracts/continuity/**",
"contracts/commit/**",
"contracts/capability/**",
"modules/continuity/**",
"modules/commit/**",
"adapters/postgres/**",
"database/bootstrap/**",
"database/migrations/**",
"cmd/continuity-runtime/**",
"cmd/commit-runtime/**",
"cmd/core-runtime/**",
"cmd/creator-web/**",
"tests/owner_acl/**",
"tests/cognitive_advisory/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000065-assistant-presence-attention-human-surface.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/ARCHITECTURE_CONSTITUTION.md",
"security/**",
"ci/**",
".gitea/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 1,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0
},
"contracts": {
"consume": [
"Continuity ProspectiveItem",
"Commit prospective notification operation",
"Capability notification binding",
"Interaction ChatMessage",
"Authority Creator grant",
"CredentialRef Secret Resolver foundation"
],
"may_change": [
"contracts/continuity@v1",
"contracts/commit@v1"
]
},
"semantic_owner": "continuity",
"canonical_state": [
"ProspectiveItem",
"AttentionItem"
],
"state_transitions": [
"ProspectiveItem ARMED -> TRIGGERED -> REVALIDATING -> DELIVERED for due reminder processing",
"AttentionItem PENDING -> DELIVERING -> DELIVERED for human contact outcome",
"No ACTIVE cognitive binding promotion",
"No OpenAI activation"
],
"side_effects_and_privileges": [
"Continuity runtime polls canonical prospective state and does not own external mutation",
"Commit runtime remains the only outbound notification executor boundary",
"Notification delivery uses existing CapabilityBinding and idempotent receiver protocol",
"Human Assistant UI reads canonical runtime APIs and hides internal identifiers by default",
"Docker-test remains prohibited"
],
"persistence_and_migrations": [
"continuity owner-local attention_items table",
"no cross-owner FK",
"no cross-owner SQL join dependency",
"Commit authorized action and attempt persistence remains Commit-owned",
"Notification receipt persistence remains receiver-owned delivery evidence"
],
"failure_retry_restart": [
"Due detection reads ARMED rows from canonical PostgreSQL state",
"CAS/unique source constraints prevent duplicate AttentionItem creation",
"Successful delivery moves AttentionItem to DELIVERED and prevents repeated delivery",
"Unavailable LLM does not block deterministic reminder firing",
"Unavailable notification delivery records non-success without inventing completion"
],
"assumptions": [
"DEV-000064 is production baseline",
"Continuity is canonical owner of future obligations and attention state for this generation",
"Commit and existing notification receiver remain the outbound mutation path",
"The first human notification channel is the existing production receiver/Windows display path",
"CPU CANARY may remain available but reminder firing is deterministic and LLM-free"
],
"acceptance_scenarios": [
"Reminder becomes due",
"Due reminder produces governed AttentionItem",
"Attention can produce notification through Commit",
"Delivery outcome is persisted",
"No duplicate delivery after success",
"Runtime restart does not lose an ARMED reminder",
"Cancelled/suppressed reminder is not delivered",
"LLM unavailable does not prevent deterministic reminder delivery",
"No cognitive model is required merely to fire a known reminder",
"CapabilityGap is rendered human-readable",
"Human UI requires no internal IDs",
"Operator Console remains available",
"Authority boundaries remain unchanged",
"Owner ACL passes",
"LLM-free audit passes",
"Production live delayed reminder delivers once",
"Today/Attention surface is built from canonical state"
],
"required_tests": [
"go test ./...",
"go vet ./...",
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"go run ./cmd/devctl check-architecture",
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
"production live delayed reminder acceptance",
"production /chat and /healthz verification"
],
"terminal_gap_states": [
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT"
]
}
+44 -5
View File
@@ -1,17 +1,56 @@
# Assistant project status
Audit date: 2026-08-17. Active implementation: DEV-000063 revision 1.
Audit date: 2026-08-17. Active implementation: DEV-000065 revision 1.
## DEV-000065 checkpoint
- `DEV_000065_STATUS = COMPLETED`
- `LAST_COMPLETED = ADR-0014 accepted; Continuity-owned AttentionItem contract and persistence; due reminder scanner; Commit internal prospective notification endpoint; human-facing Chat / Today / History surface; relative reminder parsing for "через N минут"; deterministic due-delivery regression tests; notification binding origin corrected to assistant.cin.su revision 2`
- `ADR_STATUS = ADR-0014 accepted`
- `OWNERSHIP_STATUS = no new owner; AttentionItem belongs to Continuity; notification delivery remains Commit/Capability/receiver path`
- `REMINDER_LIFECYCLE_STATUS = ARMED -> TRIGGERED -> REVALIDATING -> DELIVERED implemented for due scanner`
- `REMINDER_DELIVERY_STATUS = PASS: production live delayed reminder delivered through Commit deliver-prospective-notification-v1 and immutable receiver receipt`
- `ATTENTION_STATUS = typed AttentionItem PENDING -> DELIVERING -> DELIVERED foundation implemented`
- `HUMAN_SURFACE_STATUS = /chat remains primary; Today/Attention and History added; Operator Console preserved at /`
- `AUTHORITY_STATUS = external notification still requires Commit authorization, current Creator authority context, and active Capability notification binding`
- `LLM_FREE_STATUS = deterministic reminder firing does not require cognitive model; LLM-free env policy regression PASS`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; production live delayed reminder acceptance; production /chat attention and capability-gap acceptance`
- `LIVE_ACCEPTANCE = PASS: "Напомни мне через 3 минуты проверить уведомление." persisted ARMED reminder, transitioned to DELIVERED, generated AttentionItem, delivered notification through Commit, persisted SUCCEEDED attempt and receiver receipt, duplicate counts action_attempts=1 attention_items=1; "Отправь Ивану письмо." returned readable capability gap; "Что сейчас требует моего внимания?" answered from canonical Today with cognition=not_requested`
- `PRODUCTION_STATUS = healthy`
- `PRODUCTION_REVISION = origin/main 1113ced623896f1498d606653d71f36c361e9bb0; creator-web deployed at 1113ced623896f1498d606653d71f36c361e9bb0; continuity-runtime deployed at 2ec53e40cf19c609193c1aa74955573cac7abfe1; core-runtime/commit-runtime deployed at a0dce634f3c6efec0481f730c9dcc12797749ec4`
- `KNOWN_LIMITATION = one early DEV-000065 live probe occurred before notification binding origin revision 2 and produced UNKNOWN_OUTCOME; later regression prevents UNKNOWN_OUTCOME from being marked as delivered`
- `NEXT_STEP = Human response operations for attention/reminders: Done/Snooze/Cancel semantic commands, if separately approved`
## DEV-000064 checkpoint
- `DEV_000064_STATUS = COMPLETED`
- `LAST_COMPLETED = ADR-0013, Capability-owned generic implementation registry/resolution/selection/gap contracts, deterministic evidence-weighted resolver V1, capability gap acquisition strategy foundation, generic capability usage observations, Continuity reminder metadata, authenticated creator reminder registration endpoint, /chat reminder scheduling path, /chat SEND_EMAIL gap path, capability-aware "what can you do" response`
- `CAPABILITY_REGISTRY_STATUS = generic CapabilityImplementation + CapabilityGap + CapabilityUsageObservation foundation added under existing capability owner`
- `COGNITIVE_RELATION_STATUS = cognition remains resolved/invoked through existing Cognitive Registry/Gateway; no parallel cognitive router introduced`
- `REMINDER_CAPABILITY_STATUS = internal-reminder-scheduler-v1 seeded as VALIDATED/ACTIVE/AVAILABLE implementation for SCHEDULE_REMINDER`
- `CAPABILITY_GAP_STATUS = unsupported SEND_EMAIL persists CapabilityGap with OriginalHumanIntent/Utterance correlation`
- `AUTHORITY_STATUS = capability and CredentialRef do not grant Authority; reminder registration requires authenticated creator session and creates only Continuity-owned prospective state`
- `ACTIVE_COGNITIVE_BINDINGS = none; CPU CANARY unchanged`
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; production live /chat acceptance`
- `LIVE_ACCEPTANCE = PASS: reminder created through external /chat and verified as Continuity ARMED prospective item; unsupported SEND_EMAIL created CapabilityGap; "Привет. Что ты умеешь?" returned capability-aware response; simple code explanation completed through Cognitive Gateway without durable Work`
- `PRODUCTION_REVISION = 8fe596264e89b8f346c9e0cd297c5ccb2d5f76bb`
- `PRODUCTION_HEALTH = healthy`
- `NEXT_STEP = Direct delivery/revalidation loop for reminders or next concrete capability adapter, if separately approved`
## DEV-000063 checkpoint
- `DEV_000063_STATUS = IN_PROGRESS`
- `LAST_COMPLETED = ADR-0012, HelpPath optionality foundation, Work-owned Goal/PlanCandidate/WorkPlan/PlanStep/CapabilityRequirement/DelegationCandidate contracts, deterministic planning V1, optional durable Work creation from /chat, capability-gap/self-extension readiness tests`
- `DEV_000063_STATUS = COMPLETED`
- `LAST_COMPLETED = ADR-0012, HelpPath optionality foundation, Work-owned Goal/PlanCandidate/WorkPlan/PlanStep/CapabilityRequirement/DelegationCandidate contracts, deterministic planning V1, optional durable Work creation from /chat, capability-gap/self-extension readiness tests, migration 017, production deployment and live /chat acceptance`
- `HELP_PATH_OPTIONALITY = enabled; simple answer/cognition/direct capability/capability gap can bypass durable Goal/Work/Plan`
- `GOAL_WORK_PLAN_OPTIONALITY = enabled; durable records are created only for complex work/planning cases`
- `PROJECT_NEUTRALITY = project context is optional opaque Work reference; Project is not mandatory context`
- `CODEX_NEUTRALITY = no Codex-specific canonical entity or executor selection`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1`
- `NEXT_STEP = commit, push, PR/promotion, migration 017, production deploy, live /chat acceptance`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; production live /chat acceptance`
- `LIVE_ACCEPTANCE = PASS: simple help uses cognition without Work; reminder uses direct scheduling-capability path without Work; complex УПО investigation creates one Work Goal/Plan; capability gap remains executor-neutral without Work`
- `PRODUCTION_REVISION = 9d50a3ec635782b59ab3335808d6d1e026be48a4`
- `PRODUCTION_HEALTH = healthy`
- `NEXT_STEP = DEV-000064 capability resolution/acquisition or concrete scheduler/mail/codex adapter foundation, if separately approved`
- `REAL_BLOCKERS = none known`
## DEV-000062 checkpoint
+192
View File
@@ -0,0 +1,192 @@
package domain
import (
"errors"
"slices"
"sort"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/platform/ids"
)
var ErrInvalidCapabilityImplementation = errors.New("invalid capability implementation")
var ErrInvalidCapabilityRequirement = errors.New("invalid capability requirement")
var ErrNoCapabilityImplementation = errors.New("no eligible capability implementation")
func ValidateCapabilityImplementation(value capability.CapabilityImplementation) error {
if value.ID == "" || value.CapabilityID == "" || value.CapabilityKind == "" || value.OwnerPrincipalID == "" ||
value.Revision < 1 || value.Kind == "" || value.TargetRef == "" || value.AuthorityPolicyRef == "" ||
value.ObservedAtUnixMillis < 1 || !validQualification(value.Qualification) || !validActivation(value.Activation) ||
!validHealth(value.Health) {
return ErrInvalidCapabilityImplementation
}
switch value.Kind {
case capability.ImplementationInternalFunction, capability.ImplementationCognitiveBinding, capability.ImplementationExternalAdapter, capability.ImplementationAgent:
default:
return ErrInvalidCapabilityImplementation
}
for _, constraint := range value.DataConstraints {
switch constraint {
case capability.DataConstraintLocalOnly, capability.DataConstraintExternalAllowed, capability.DataConstraintNoCredentialForward:
default:
return ErrInvalidCapabilityImplementation
}
}
return nil
}
func ResolveCapability(requirement capability.CapabilityResolutionRequirement, candidates []capability.CapabilityImplementation) (capability.CapabilityResolution, error) {
if requirement.CapabilityKind == "" || requirement.OwnerPrincipalID == "" || requirement.OriginalIntentID == "" || requirement.SourceUtteranceID == "" || requirement.ObservedAtUnixMillis < 1 {
return capability.CapabilityResolution{}, ErrInvalidCapabilityRequirement
}
resolutionID, err := ids.New()
if err != nil {
return capability.CapabilityResolution{}, err
}
resolution := capability.CapabilityResolution{
ID: capability.CapabilityResolutionID(resolutionID),
Requirement: requirement,
Status: capability.ResolutionUnavailable,
ExcludedReasons: map[capability.CapabilityImplementationID]capability.GapReason{},
ObservedAtUnixMillis: requirement.ObservedAtUnixMillis,
}
eligible := make([]capability.CapabilityImplementation, 0, len(candidates))
anyForCapability := false
dominantReason := capability.GapNoImplementation
for _, candidate := range candidates {
if candidate.CapabilityKind != requirement.CapabilityKind {
continue
}
if requirement.CapabilityID != "" && candidate.CapabilityID != requirement.CapabilityID {
continue
}
anyForCapability = true
reason := exclusionReason(requirement, candidate)
if reason != "" {
resolution.ExcludedReasons[candidate.ID] = reason
if dominantReason == capability.GapNoImplementation {
dominantReason = reason
}
continue
}
eligible = append(eligible, candidate)
}
if len(eligible) == 0 {
if !anyForCapability {
dominantReason = capability.GapNoImplementation
}
gap, err := BuildCapabilityGap(requirement, dominantReason, gapDetail(dominantReason), requirement.ObservedAtUnixMillis)
if err != nil {
return capability.CapabilityResolution{}, err
}
resolution.Gap = &gap
return resolution, ErrNoCapabilityImplementation
}
sort.SliceStable(eligible, func(i, j int) bool { return betterImplementation(eligible[i], eligible[j]) })
resolution.Status = capability.ResolutionAvailable
resolution.Eligible = make([]capability.ResolvedCapabilityImplementation, 0, len(eligible))
for index, item := range eligible {
resolution.Eligible = append(resolution.Eligible, capability.ResolvedCapabilityImplementation{Implementation: item, Selected: index == 0, Rank: index + 1})
}
return resolution, nil
}
func BuildCapabilityGap(requirement capability.CapabilityResolutionRequirement, reason capability.GapReason, detail string, millis int64) (capability.CapabilityGap, error) {
id, err := ids.New()
if err != nil {
return capability.CapabilityGap{}, err
}
return capability.CapabilityGap{
ID: capability.CapabilityGapID(id),
RequirementID: requirement.RequirementID,
CapabilityKind: requirement.CapabilityKind,
CapabilityID: requirement.CapabilityID,
OwnerPrincipalID: requirement.OwnerPrincipalID,
OriginalIntentID: requirement.OriginalIntentID,
SourceUtteranceID: requirement.SourceUtteranceID,
Reason: reason,
Detail: detail,
Strategies: acquisitionStrategies(reason),
RecordedAtUnixMillis: millis,
}, nil
}
func exclusionReason(requirement capability.CapabilityResolutionRequirement, candidate capability.CapabilityImplementation) capability.GapReason {
if ValidateCapabilityImplementation(candidate) != nil {
return capability.GapIncompatible
}
if candidate.OwnerPrincipalID != requirement.OwnerPrincipalID {
return capability.GapUnauthorized
}
if candidate.Qualification != capability.QualificationValidated {
return capability.GapUnqualified
}
if candidate.Activation != capability.ActivationActive && !(requirement.AllowCanary && candidate.Activation == capability.ActivationCanary) {
return capability.GapInactive
}
if candidate.Health != capability.HealthAvailable {
return capability.GapUnhealthy
}
if requirement.AuthorityContextRef != "" && candidate.AuthorityPolicyRef != requirement.AuthorityContextRef {
return capability.GapUnauthorized
}
for _, required := range requirement.RequiredConstraints {
if !slices.Contains(candidate.DataConstraints, required) {
return capability.GapIncompatible
}
}
return ""
}
func betterImplementation(left, right capability.CapabilityImplementation) bool {
if left.ReliabilityMillis != nil && right.ReliabilityMillis != nil && *left.ReliabilityMillis != *right.ReliabilityMillis {
return *left.ReliabilityMillis > *right.ReliabilityMillis
}
if left.LatencyScoreMillis != nil && right.LatencyScoreMillis != nil && *left.LatencyScoreMillis != *right.LatencyScoreMillis {
return *left.LatencyScoreMillis < *right.LatencyScoreMillis
}
if left.CostScoreMillis != nil && right.CostScoreMillis != nil && *left.CostScoreMillis != *right.CostScoreMillis {
return *left.CostScoreMillis < *right.CostScoreMillis
}
return string(left.ID) < string(right.ID)
}
func gapDetail(reason capability.GapReason) string {
switch reason {
case capability.GapNoImplementation:
return "no registered implementation satisfies this capability requirement"
case capability.GapUnhealthy:
return "registered implementations are currently unavailable or degraded"
case capability.GapInactive:
return "registered implementations are not active for this use"
case capability.GapUnqualified:
return "registered implementations are not validated"
case capability.GapUnauthorized:
return "registered implementations are not allowed by the current authority or policy context"
case capability.GapIncompatible:
return "registered implementations do not match the required modality, data or environment constraints"
default:
return "capability availability is unknown"
}
}
func acquisitionStrategies(reason capability.GapReason) []capability.AcquisitionStrategyCandidate {
switch reason {
case capability.GapNoImplementation:
return []capability.AcquisitionStrategyCandidate{
{Kind: capability.AcquireDiscoverExisting, Reason: "look for an already available adapter or service"},
{Kind: capability.AcquireConnectExisting, Reason: "connect an existing provider after explicit authority"},
{Kind: capability.AcquireBuildNew, Reason: "build a new implementation through a future approved coding capability"},
}
case capability.GapUnhealthy:
return []capability.AcquisitionStrategyCandidate{
{Kind: capability.AcquireUseAlternative, Reason: "try another eligible implementation"},
{Kind: capability.AcquireConfigureExisting, Reason: "repair or reconfigure the registered implementation"},
}
case capability.GapUnauthorized:
return []capability.AcquisitionStrategyCandidate{{Kind: capability.AcquireRequestUserInput, Reason: "request explicit authority or policy decision"}}
default:
return []capability.AcquisitionStrategyCandidate{{Kind: capability.AcquireUnresolvableCurrently, Reason: strings.ToLower(gapDetail(reason))}}
}
}
@@ -0,0 +1,98 @@
package domain
import (
"errors"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestReminderResolvesToSchedulingImplementation(t *testing.T) {
requirement := resolutionRequirement(capability.CapabilityKindScheduleReminder)
resolution, err := ResolveCapability(requirement, []capability.CapabilityImplementation{implementation("scheduler-b", capability.CapabilityKindScheduleReminder, 900, 50), implementation("scheduler-a", capability.CapabilityKindScheduleReminder, 950, 75)})
if err != nil {
t.Fatal(err)
}
if resolution.Status != capability.ResolutionAvailable || len(resolution.Eligible) != 2 || !resolution.Eligible[0].Selected || resolution.Eligible[0].Implementation.ID != "scheduler-a" {
t.Fatalf("unexpected resolution: %#v", resolution)
}
}
func TestUnavailableImplementationExcluded(t *testing.T) {
candidate := implementation("scheduler", capability.CapabilityKindScheduleReminder, 900, 50)
candidate.Health = capability.HealthUnavailable
resolution, err := ResolveCapability(resolutionRequirement(capability.CapabilityKindScheduleReminder), []capability.CapabilityImplementation{candidate})
if !errors.Is(err, ErrNoCapabilityImplementation) || resolution.Gap == nil || resolution.Gap.Reason != capability.GapUnhealthy {
t.Fatalf("expected unhealthy gap, got resolution=%#v err=%v", resolution, err)
}
}
func TestSelectionDoesNotDependOnProviderName(t *testing.T) {
left := implementation("zzz-provider-name", capability.CapabilityKindScheduleReminder, 980, 20)
right := implementation("aaa-provider-name", capability.CapabilityKindScheduleReminder, 900, 10)
resolution, err := ResolveCapability(resolutionRequirement(capability.CapabilityKindScheduleReminder), []capability.CapabilityImplementation{left, right})
if err != nil {
t.Fatal(err)
}
if resolution.Eligible[0].Implementation.ID != left.ID {
t.Fatalf("selection appears provider-name driven: %#v", resolution.Eligible)
}
}
func TestCredentialRefDoesNotGrantAuthority(t *testing.T) {
candidate := implementation("mail", capability.CapabilityKindSendEmail, 900, 50)
candidate.CredentialRef = "credential://mail/token"
candidate.AuthorityPolicyRef = "mail-send-approved-v1"
resolution, err := ResolveCapability(resolutionRequirement(capability.CapabilityKindSendEmail), []capability.CapabilityImplementation{candidate})
if !errors.Is(err, ErrNoCapabilityImplementation) || resolution.Gap == nil || resolution.Gap.Reason != capability.GapUnauthorized {
t.Fatalf("credential ref must not grant authority: resolution=%#v err=%v", resolution, err)
}
}
func TestCapabilityGapPreservesOriginalIntentCorrelation(t *testing.T) {
requirement := resolutionRequirement(capability.CapabilityKindSendEmail)
resolution, err := ResolveCapability(requirement, nil)
if !errors.Is(err, ErrNoCapabilityImplementation) || resolution.Gap == nil {
t.Fatalf("expected gap, got resolution=%#v err=%v", resolution, err)
}
if resolution.Gap.OriginalIntentID != requirement.OriginalIntentID || resolution.Gap.SourceUtteranceID != requirement.SourceUtteranceID || len(resolution.Gap.Strategies) == 0 {
t.Fatalf("gap lost correlation/strategies: %#v", resolution.Gap)
}
if strings.Contains(strings.ToLower(resolution.Gap.Detail), "codex") {
t.Fatalf("gap must not become codex-specific: %#v", resolution.Gap)
}
}
func resolutionRequirement(kind capability.CapabilityKind) capability.CapabilityResolutionRequirement {
return capability.CapabilityResolutionRequirement{
CapabilityKind: kind,
OwnerPrincipalID: "owner-1",
ActorID: "actor-1",
OriginalIntentID: "intent-1",
SourceUtteranceID: "utterance-1",
RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly},
AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1,
ObservedAtUnixMillis: 1000,
}
}
func implementation(id string, kind capability.CapabilityKind, reliability int64, latency int64) capability.CapabilityImplementation {
return capability.CapabilityImplementation{
ID: capability.CapabilityImplementationID(id),
CapabilityID: capability.CapabilityID(strings.ToLower(string(kind))),
CapabilityKind: kind,
OwnerPrincipalID: "owner-1",
Revision: 1,
Kind: capability.ImplementationInternalFunction,
TargetRef: capability.ImplementationTargetRef("internal://" + strings.ToLower(string(kind))),
Qualification: capability.QualificationValidated,
Activation: capability.ActivationActive,
Health: capability.HealthAvailable,
AuthorityPolicyRef: capability.AuthorityPolicyCreatorDirectiveV1,
DataConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly, capability.DataConstraintNoCredentialForward},
ReliabilityMillis: &reliability,
LatencyScoreMillis: &latency,
ObservedAtUnixMillis: 1000,
}
}
+9 -2
View File
@@ -44,10 +44,17 @@ func Reconcile(action commitcontract.AuthorizedAction, previous commitcontract.A
// Authorize is the Commit boundary. A proposal alone never reaches Executor.
func Authorize(proposal commitcontract.ActionProposal, context AuthorizationContext, validator ports.AuthorityValidator, authorizedID commitcontract.AuthorizedActionID, authorizedAtUnixMillis int64) (commitcontract.AuthorizedAction, error) {
if proposal.IntentID == "" || proposal.ActorID == "" || proposal.CapabilityID == capability.CapabilityID("") ||
proposal.Target.TaskID == "" || proposal.Target.GoalID == "" || proposal.Target.ScopeID == "" || proposal.Target.Revision == 0 || proposal.Operation == "" || authorizedAtUnixMillis <= 0 {
proposal.Operation == "" || authorizedAtUnixMillis <= 0 {
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
}
if proposal.ActorID != context.Binding.ActorID || proposal.Target.ScopeID != context.Scope.ID {
if proposal.ActorID != context.Binding.ActorID {
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
}
if proposal.Operation == commitcontract.OperationDeliverProspectiveNotification {
if proposal.Target.ScopeID != "" && proposal.Target.ScopeID != context.Scope.ID {
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
}
} else if proposal.Target.TaskID == "" || proposal.Target.GoalID == "" || proposal.Target.ScopeID == "" || proposal.Target.Revision == 0 || proposal.Target.ScopeID != context.Scope.ID {
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
}
if !allowedOperation(proposal, context) {
+46
View File
@@ -12,6 +12,8 @@ var (
ErrProspectiveRevision = errors.New("prospective item revision conflict")
ErrInvalidTransition = errors.New("invalid prospective item transition")
ErrInvalidSatisfaction = errors.New("invalid delivered satisfaction command")
ErrInvalidReminder = errors.New("invalid reminder request")
ErrInvalidAttention = errors.New("invalid attention item")
)
type Store struct {
@@ -28,6 +30,50 @@ func ValidateSatisfyDelivered(command contract.SatisfyDeliveredCommand) error {
return nil
}
func BuildReminder(request contract.ScheduleReminderRequest, id contract.ProspectiveItemID) (contract.ProspectiveItem, error) {
if id == "" || request.OwnerPrincipalID == "" || request.ScopeID == "" || strings.TrimSpace(request.SourceIntentID) == "" ||
strings.TrimSpace(request.SourceUtteranceID) == "" || strings.TrimSpace(request.Summary) == "" ||
request.TriggerAtUnixMillis <= request.RequestedAtUnixMillis || request.RequestedAtUnixMillis < 1 {
return contract.ProspectiveItem{}, ErrInvalidReminder
}
return contract.ProspectiveItem{
ID: id,
CommitmentID: contract.CommitmentID(""),
OwnerPrincipalID: request.OwnerPrincipalID,
ScopeID: request.ScopeID,
GoalID: "",
SourceIntentID: strings.TrimSpace(request.SourceIntentID),
SourceUtteranceID: strings.TrimSpace(request.SourceUtteranceID),
Summary: strings.TrimSpace(request.Summary),
TriggerAtUnixMillis: request.TriggerAtUnixMillis,
State: contract.ProspectiveArmed,
Revision: 1,
}, nil
}
func BuildDueReminderAttention(item contract.ProspectiveItem, id contract.AttentionItemID, nowUnixMillis int64) (contract.AttentionItem, error) {
if id == "" || item.ID == "" || item.OwnerPrincipalID == "" || item.ScopeID == "" ||
strings.TrimSpace(item.Summary) == "" || item.TriggerAtUnixMillis < 1 || nowUnixMillis < 1 {
return contract.AttentionItem{}, ErrInvalidAttention
}
if item.State != contract.ProspectiveDelivered && item.State != contract.ProspectiveRevalidating {
return contract.AttentionItem{}, ErrInvalidAttention
}
return contract.AttentionItem{
ID: id,
OwnerPrincipalID: item.OwnerPrincipalID,
ScopeID: item.ScopeID,
SourceKind: contract.AttentionSourceProspectiveReminder,
SourceProspectiveID: item.ID,
Level: contract.AttentionDue,
State: contract.AttentionPending,
Summary: item.Summary,
DueAtUnixMillis: item.TriggerAtUnixMillis,
CreatedAtUnixMillis: nowUnixMillis,
Revision: 1,
}, nil
}
func (s *Store) SatisfyDelivered(id contract.ProspectiveItemID, revision contract.ProspectiveRevision, evidenceRef string) (contract.ProspectiveItem, error) {
item, err := s.current(id, revision)
if err != nil {
+8
View File
@@ -4,9 +4,17 @@ import (
"context"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
)
type Repository interface {
Save(context.Context, continuity.ProspectiveItem) error
Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error)
DueArmed(context.Context, int64, int) ([]continuity.ProspectiveItem, error)
ClaimDueReminder(context.Context, continuity.ProspectiveItemID, continuity.ProspectiveRevision, continuity.AttentionItemID, int64) (continuity.ProspectiveItem, continuity.AttentionItem, error)
PendingAttention(context.Context, int) ([]continuity.AttentionItem, error)
MarkAttentionDelivering(context.Context, continuity.AttentionItemID, continuity.AttentionRevision) (continuity.AttentionItem, error)
MarkAttentionDelivered(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string, int64) (continuity.AttentionItem, error)
MarkAttentionFailed(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string) (continuity.AttentionItem, error)
AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error)
}
+1 -1
View File
@@ -105,7 +105,7 @@ func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) in
return interaction.SpeechActQuestion
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
return interaction.SpeechActRequestToInvestigate
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить"):
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить") || strings.Contains(lower, "напомни") || strings.Contains(lower, "подключи"):
return interaction.SpeechActDirective
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
return interaction.SpeechActDecision
+13
View File
@@ -36,6 +36,19 @@ func TestNewHumanUtteranceAppendsCorrectionRelation(t *testing.T) {
}
}
func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t *testing.T) {
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
for _, message := range []string{"Напомни завтра позвонить врачу.", "Подключи интеграцию с неизвестным сервисом."} {
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Unix(10, 0))
if err != nil {
t.Fatal(err)
}
if utterance.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive for %q, got %s", message, utterance.SpeechAct)
}
}
}
func TestNewConversationRequiresActorAndPrincipal(t *testing.T) {
_, err := NewConversation(identity.ActorID(""), identity.PrincipalID("principal-1"), time.Unix(10, 0))
if err == nil {
+2
View File
@@ -78,6 +78,8 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
case containsAny(lower, "напомни", "напомин"):
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
case containsAny(lower, "отправь", "письм", "почт"):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."}
case containsAny(lower, "не умеешь", "подключи интеграцию", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер"):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
case requiresDurableWork(lower):