Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
61774dc
Reconcile uncertain personal plan operations before retry
Chris0Jeky Sep 10, 2026
f31421b
Retain resolved Auto appearance in comparison observations
Chris0Jeky Sep 10, 2026
cbf0ce2
Record personal plan recovery qualification
Chris0Jeky Sep 10, 2026
7514b43
Keep written audio drafts bound to their original question
Chris0Jeky Sep 10, 2026
8e948b3
Record personal Focus after accepted navigation
Chris0Jeky Sep 10, 2026
b091423
Record written audio draft recovery qualification
Chris0Jeky Sep 10, 2026
f9ab859
Retract original selections when memory revisions conflict
Chris0Jeky Sep 10, 2026
c39bb69
Validate source storage limits before API startup
Chris0Jeky Sep 10, 2026
0494e68
Record source-storage startup validation proof
Chris0Jeky Sep 10, 2026
444b4e3
Generate private questions from selected card evidence
Chris0Jeky Sep 10, 2026
4294801
Preview model evidence and review grounded questions across experiences
Chris0Jeky Sep 10, 2026
19edf40
Record grounded question contract and verification limits
Chris0Jeky Sep 10, 2026
9bba017
Integrate resolved appearance comparison follow-through
Chris0Jeky Sep 10, 2026
116c1b2
Integrate accepted focus and uncertain plan recovery
Chris0Jeky Sep 10, 2026
b05444d
Integrate retained written-audio draft recovery
Chris0Jeky Sep 10, 2026
d2b7c2d
Integrate original selection conflict recovery
Chris0Jeky Sep 10, 2026
d4d61a0
Integrate source-storage startup validation
Chris0Jeky Sep 10, 2026
652ee5d
Integrate explicit grounded model questions
Chris0Jeky Sep 10, 2026
a2c2336
Bound chat receipt recovery without resending messages
Chris0Jeky Sep 10, 2026
7ad8bf1
Expose eligible board previews across Review origins
Chris0Jeky Sep 10, 2026
d6369c1
Merge branch 'codex/2808-overhaul-integration' into codex/2808-overha…
Chris0Jeky Sep 10, 2026
276a997
Record combined overhaul follow-through and current source interfaces
Chris0Jeky Sep 10, 2026
c229607
Match audio confirmation retries to immutable request receipts
Chris0Jeky Sep 10, 2026
e83bb67
Document exact audio confirmation recovery
Chris0Jeky Sep 10, 2026
7222855
Bind audio playback responses to the current recording receipt
Chris0Jeky Sep 10, 2026
706685b
Record audio playback recovery validation
Chris0Jeky Sep 10, 2026
27aa36d
Record full audio confirmation qualification
Chris0Jeky Sep 10, 2026
63b7ff9
Retain confirmation history after a recording loses its board
Chris0Jeky Sep 10, 2026
fc50406
Mark source and destination lanes for proposed card moves
Chris0Jeky Sep 10, 2026
cb75026
Add an opt-in account budget for existing question reminders
Chris0Jeky Sep 10, 2026
921726b
Offer quiet reminders with focus and input suppression
Chris0Jeky Sep 10, 2026
e6da15a
Merge audio playback recovery into overhaul follow-through
Chris0Jeky Sep 10, 2026
55e19ca
Merge exact audio receipts and retained preview continuity
Chris0Jeky Sep 10, 2026
5773d14
Qualify optional reminders and align controller conventions
Chris0Jeky Sep 10, 2026
58301fc
Merge opt-in reminders with shared attention budget
Chris0Jeky Sep 10, 2026
25e2c7d
Integrate qualified storage repair and current CI base
Chris0Jeky Sep 10, 2026
6c6a344
Reconcile combined migration model and prove both move lanes
Chris0Jeky Sep 10, 2026
66b082a
Normalize audio filenames before retry comparison
Chris0Jeky Sep 10, 2026
46d592a
Merge normalized audio upload retry receipts
Chris0Jeky Sep 10, 2026
d1098fd
Record expanded overhaul qualification and delivery limits
Chris0Jeky Sep 10, 2026
d3b6a29
Fence grounded question commits and preserve accounting outcomes
Chris0Jeky Sep 10, 2026
1ccf059
Document grounded question commit recovery evidence
Chris0Jeky Sep 10, 2026
6ef089b
Stop thinking workspace reads after route exit
Chris0Jeky Sep 10, 2026
6f7dc36
Record thinking route recovery qualification
Chris0Jeky Sep 10, 2026
061e662
Define bounded transcription attempts and daily admission
Chris0Jeky Sep 10, 2026
40b429d
Process explicit audio requests into private provisional transcripts
Chris0Jeky Sep 10, 2026
98ef755
Review and recover explicit transcripts across workspace experiences
Chris0Jeky Sep 10, 2026
b0fb672
Preserve transcription lineage in confirmation metadata and inventory
Chris0Jeky Sep 10, 2026
eab4935
Document explicit transcription delivery evidence and provider limits
Chris0Jeky Sep 10, 2026
0d0f00d
Record default-off browser proof and transcription entry points
Chris0Jeky Sep 10, 2026
ffd96e0
Bind recording transcription consent to the displayed configuration
Chris0Jeky Sep 10, 2026
cdcadd6
Reconcile transcription behavior and consent verification
Chris0Jeky Sep 10, 2026
3518d7e
Distinguish observation storage contention from changed evidence
Chris0Jeky Sep 10, 2026
47794df
Bring reviewed integration baseline into overhaul follow-through
Chris0Jeky Sep 10, 2026
bc785c4
Integrate grounded commit and storage recovery outcomes
Chris0Jeky Sep 10, 2026
0e4d0e2
Integrate thinking workspace navigation recovery
Chris0Jeky Sep 10, 2026
ea8c0cb
Integrate explicit recording transcription and consent recovery
Chris0Jeky Sep 10, 2026
948751c
Reject grounded observation commits after account erasure
Chris0Jeky Sep 10, 2026
ba6e2cd
Record integrated overhaul proof and source-backed remaining scope
Chris0Jeky Sep 10, 2026
fce97dc
Align overhaul continuation with merged integration
Chris0Jeky Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions autodoc/AGENT_INDEX.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Taskdeck.Api.Extensions;
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Interfaces;
using Taskdeck.Application.Services;

namespace Taskdeck.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/thinking-audio/{id:guid}/transcriptions")]
public class AudioTranscriptionController : AuthenticatedControllerBase
{
private readonly AudioTranscriptionService service;
public AudioTranscriptionController(AudioTranscriptionService service, IUserContext userContext) : base(userContext)
{ this.service = service; }

[HttpGet]
public async Task<IActionResult> Status(Guid id, CancellationToken ct)
{
if (!TryGetCurrentUserId(out var userId, out var error)) return error!;
Response.Headers.CacheControl = "no-store";
var result = await service.StatusAsync(userId, id, ct);
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}

[HttpPost]
public async Task<IActionResult> Start(Guid id, AudioTranscriptionRequestDto request, CancellationToken ct)
{
if (!TryGetCurrentUserId(out var userId, out var error)) return error!;
Response.Headers.CacheControl = "no-store";
var result = await service.StartAsync(userId, id, request, ct);
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Taskdeck.Api.Extensions;
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Interfaces;
using Taskdeck.Application.Services;

namespace Taskdeck.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/workspace-attention")]
public class WorkspaceAttentionController : AuthenticatedControllerBase
{
private readonly WorkspaceAttentionService service;

public WorkspaceAttentionController(WorkspaceAttentionService service, IUserContext context) : base(context)
{
this.service = service;
}

[HttpGet]
public async Task<IActionResult> Get(CancellationToken ct)
{
if (!TryGetCurrentUserId(out var user, out var error)) return error!;
return Ok(await service.GetAsync(user, ct));
}
[HttpPut]
public async Task<IActionResult> Save(SaveWorkspaceAttentionDto dto, CancellationToken ct)
{
if (!TryGetCurrentUserId(out var user, out var error)) return error!;
var result = await service.SaveAsync(user, dto, ct);
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}
[HttpPost("claim")]
public async Task<IActionResult> Claim(ClaimWorkspaceAttentionDto dto, CancellationToken ct)
{
if (!TryGetCurrentUserId(out var user, out var error)) return error!;
var result = await service.ClaimAsync(user, dto.BoardId, ct);
return !result.IsSuccess ? result.ToErrorActionResult() : result.Value is null ? NoContent() : Ok(result.Value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ public async Task<IActionResult> Analyze(AnalyzeWorkspaceDto dto, CancellationTo
var result = await service.ListAsync(user, dto.BoardId, true, ct);
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}
[HttpGet("observation-source")]
public async Task<IActionResult> ObservationSource([FromQuery] Guid boardId, [FromQuery] Guid cardId,
[FromServices] WorkspaceObservationService observations, CancellationToken ct)
{
if (!TryGetCurrentUserId(out var user, out var error)) return error!;
var result = await observations.SourceAsync(user, boardId, cardId, ct);
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}
[HttpPost("model-analysis")]
public async Task<IActionResult> ModelAnalysis(GenerateObservationsDto dto,
[FromServices] WorkspaceObservationService observations, CancellationToken ct)
{
if (!TryGetCurrentUserId(out var user, out var error)) return error!;
var result = await observations.GenerateAsync(user, dto, ct);
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
}
[HttpPatch("{id:guid}")]
public async Task<IActionResult> Act(Guid id, InsightActionDto dto, CancellationToken ct)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddScoped<ThinkingDeckService>();
services.AddScoped<BoardDependencyService>();
services.AddScoped<WorkspacePlanService>();
services.AddScoped<WorkspaceAttentionService>();
services.TryAddSingleton(TimeProvider.System);
services.AddScoped<ChatContextResolver>();
services.AddScoped<ThinkingStepService>();
services.AddScoped<ThinkingAnswerService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public static IServiceCollection AddLlmProviders(
var llmKillSwitchSettings = configuration.GetSection("LlmKillSwitch").Get<LlmKillSwitchSettings>() ?? new LlmKillSwitchSettings();
services.AddSingleton(llmKillSwitchSettings);
services.AddScoped<ILlmQuotaService, LlmQuotaService>();
services.AddScoped<WorkspaceObservationService>();
services.AddSingleton<ILlmKillSwitchService, LlmKillSwitchService>();

// Abuse detection settings, shared state (singleton), and service (scoped to access ILlmUsageRecordRepository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal sealed class ProtectedOutboundSentryHttpMessageHandlerFilter : IHttpMes
nameof(OpenAiLlmProvider),
LlmProviderRegistration.OpenAiCompatibleHttpClientName,
nameof(OllamaLlmProvider),
nameof(HttpAudioTranscriptionProvider),
"OutboundWebhookDelivery"
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Taskdeck.Api.Workers;
using Taskdeck.Application.Interfaces;
using Taskdeck.Application.Services;

namespace Taskdeck.Api.Extensions;

public static class SpeechTranscriptionRegistration
{
public static IServiceCollection AddSpeechTranscription(this IServiceCollection services, IConfiguration configuration, string environment)
{
var settings = configuration.GetSection("SpeechTranscription").Get<SpeechTranscriptionSettings>() ?? new();
var policy = settings.Validate(environment);
// AddLlmProviders owns these application-wide instances; reuse the same disclosure registry and tracker.
var registry = services.Last(x => x.ServiceType == typeof(IEgressRegistry)).ImplementationInstance as IEgressRegistry
?? throw new InvalidOperationException("Register the shared provider egress registry before speech transcription.");
if (policy.Enabled) registry.Register(new(new Uri(policy.Origin).Host,
"Explicitly selected original recording for provisional transcription", nameof(HttpAudioTranscriptionProvider), EgressDataClassification.UserContent));
var tracker = (CircuitBreakerStateTracker)services.Last(x => x.ServiceType == typeof(CircuitBreakerStateTracker)).ImplementationInstance!;
var circuitSettings = (CircuitBreakerSettings)services.Last(x => x.ServiceType == typeof(CircuitBreakerSettings)).ImplementationInstance!;
var circuit = LlmProviderRegistration.BuildCircuitBreakerPolicy(tracker, "SpeechTranscription", circuitSettings);
services.AddSingleton(settings);
services.AddSingleton(policy);
services.AddScoped<AudioTranscriptionService>();
services.AddHttpClient<HttpAudioTranscriptionProvider>(client => client.Timeout = Timeout.InfiniteTimeSpan)
.ConfigurePrimaryHttpMessageHandler(sp => new SocketsHttpHandler
{
AllowAutoRedirect = false, UseProxy = false, ActivityHeadersPropagator = null,
MeterFactory = sp.GetRequiredService<ProtectedOutboundMeterFactory>(),
ConnectCallback = (context, ct) => OutboundWebhookConnectCallback.ConnectAsync(context,
environment == "Development" && settings.AllowLocalhostInDevelopment, ct)
})
.RemoveAllLoggers()
.AddHttpMessageHandler(() => new SpeechCircuitFailureHandler())
.AddPolicyHandler(circuit)
.AddHttpMessageHandler<ProtectedOutboundTelemetryHandler>()
.AddHttpMessageHandler(sp => new EgressEnvelopeHandler(sp.GetRequiredService<IEgressRegistry>(),
sp.GetRequiredService<ILogger<EgressEnvelopeHandler>>(), nameof(HttpAudioTranscriptionProvider), followRedirects: false));
services.AddScoped<IAudioTranscriptionProvider>(sp => sp.GetRequiredService<HttpAudioTranscriptionProvider>());
return services;
}

private sealed class SpeechCircuitFailureHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try { return await base.SendAsync(request, cancellationToken); }
catch (Polly.CircuitBreaker.BrokenCircuitException)
{ return new HttpResponseMessage(System.Net.HttpStatusCode.ServiceUnavailable); }
}
}
}
1 change: 1 addition & 0 deletions backend/src/Taskdeck.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@

// Add LLM providers (quota, kill switch, OpenAI/compatible/Ollama/Mock selection)
builder.Services.AddLlmProviders(builder.Configuration);
builder.Services.AddSpeechTranscription(builder.Configuration, builder.Environment.EnvironmentName);

// Add IUserContext for claim-based identity
builder.Services.AddHttpContextAccessor();
Expand Down
17 changes: 17 additions & 0 deletions backend/src/Taskdeck.Application/DTOs/AudioTranscriptionDtos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Taskdeck.Application.Services;

namespace Taskdeck.Application.DTOs;

public sealed record AudioTranscriptionRequestDto(Guid RequestId, long ExpectedRevision, string ConfigurationHash);
public sealed record AudioTranscriptionReceiptDto(Guid Id, Guid RequestId, Guid AudioAnswerId, string State,
string Provider, string Model, string ConfigurationHash, DateTimeOffset StartedAt, DateTimeOffset Deadline,
DateTimeOffset? FinishedAt, string? FailureCode, Guid? RepresentationId, string? Text);
public sealed record AudioTranscriptionStatusDto(SpeechTranscriptionConfiguration Configuration,
int AttemptsUsedToday, long InputBytesUsedToday, IReadOnlyList<AudioTranscriptionReceiptDto> Attempts);
public sealed record AudioTranscriptionSource(Guid ReferenceId, long ByteSize, string ContentHash, string MediaType);
public sealed record AudioTranscriptionAdmission(AudioTranscriptionReceiptDto Receipt, AudioTranscriptionSource? Source);
public sealed record AudioTranscriptionAttemptExportDto(Guid Id, Guid UserId, Guid AudioAnswerId, Guid CaptureId,
Guid SourceAssetId, Guid RequestId, string RequestHash, string ConfigurationHash, string Provider, string Model,
DateTimeOffset StartedAt, DateTimeOffset Deadline, DateTimeOffset? FinishedAt, string State, string? FailureCode,
Guid? RepresentationId, long Revision);
public sealed record AudioTranscriptionBudgetExportDto(Guid UserId, int UtcDay, int Attempts, long InputBytes, long Revision);
4 changes: 3 additions & 1 deletion backend/src/Taskdeck.Application/DTOs/DataPortabilityDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ public record UserDataExportPreferencesDto(
string WorkspaceMode,
DateTimeOffset CreatedAt,
Taskdeck.Domain.Entities.PersonalPlan? PersonalPlan = null,
long PersonalPlanRevision = 0);
long PersonalPlanRevision = 0,
Taskdeck.Domain.Entities.WorkspaceAttention? Attention = null,
long AttentionRevision = 0);

public record UserDataExportNotificationPreferencesDto(
bool InAppChannelEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ public sealed record SourceStorageExportDto(
IReadOnlyList<SourceBlobReferenceExportDto> References,
IReadOnlyList<SourceBlobChunkExportDto> Chunks,
IReadOnlyList<RepresentationDescriptor> Representations,
IReadOnlyList<ThinkingAudioExportDto> AudioAnswers);
IReadOnlyList<ThinkingAudioExportDto> AudioAnswers,
IReadOnlyList<AudioTranscriptionAttemptExportDto>? AudioTranscriptionAttempts = null,
IReadOnlyList<AudioTranscriptionBudgetExportDto>? AudioTranscriptionBudgets = null);
public sealed record SourceBlobObjectExportDto(Guid Id, string ContentHash, long ByteSize);
public sealed record SourceBlobReferenceExportDto(Guid Id, Guid BlobId, string Modality, string? ReferrerKind, Guid? ReferrerId, DateTimeOffset AcquiredAt);
/// <summary>Concatenate decoded chunks by ordinal, then verify the object's byte size and SHA-256.</summary>
public sealed record SourceBlobChunkExportDto(Guid BlobId, int Ordinal, byte[] Content);
public sealed record ThinkingAudioExportDto(Guid Id, Guid? BoardId, Guid CardId, Guid LayerId, string QuestionHash,
Guid CaptureId, Guid SourceAssetId, Guid UploadId, long Revision, Guid? RepresentationId, Guid? ConfirmedMemoryId,
DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt, string? ConfirmationRequestHash = null);
2 changes: 1 addition & 1 deletion backend/src/Taskdeck.Application/DTOs/ThinkingAudioDtos.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Taskdeck.Application.DTOs;

public sealed record ThinkingAudioUploadDto(Guid UploadId, long ExpectedDeckRevision, long ByteSize, string FileName);
public sealed record ThinkingAudioWriteDto(long ExpectedRevision, string Text);
public sealed record ThinkingAudioWriteDto(long ExpectedRevision, string Text, Guid? SourceRepresentationId = null);
public sealed record ThinkingAudioConfirmDto(long ExpectedRevision, long ExpectedDeckRevision, Guid RepresentationId, string Status);
public sealed record ThinkingAudioRepresentationDto(Guid Id, string Text, string Quality, Guid? SupersededById);
public sealed record ThinkingAudioDto(Guid Id, long Revision, Guid CaptureId, Guid SourceAssetId, string QuestionHash,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Taskdeck.Application.DTOs;

public sealed record WorkspaceAttentionDto(bool Enabled, long Revision, int DailyLimit, int MinimumSpacingMinutes);
public sealed record SaveWorkspaceAttentionDto(long ExpectedRevision, bool Enabled);
public sealed record ClaimWorkspaceAttentionDto(Guid BoardId);
public sealed record WorkspaceAttentionReminderDto(Guid BoardId, Guid InsightId);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace Taskdeck.Application.DTOs;

public record ObservationSourceDto(Guid CardId, string Title, string Text, string Fingerprint, bool Truncated);
public record GenerateObservationsDto(Guid BoardId, Guid CardId, [Required, StringLength(64, MinimumLength = 64)] string Fingerprint);
public record ObservationCandidate(string Kind, string Question, string Reason, string Quote);
public record ObservationEvidence(string Fingerprint, string Quote, string SourceTitle, DateTimeOffset GeneratedAt, string Provider, string Model);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Taskdeck.Application.Services;

namespace Taskdeck.Application.Interfaces;

public interface IAudioTranscriptionProvider
{
SpeechTranscriptionConfiguration Configuration { get; }
Task<AudioTranscriptionProviderResult> TranscribeAsync(byte[] original, string mediaType, CancellationToken ct);
}

public sealed record AudioTranscriptionProviderResult(string? Text, string? FailureCode);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Services;
using Taskdeck.Domain.Common;

namespace Taskdeck.Application.Interfaces;

/// <summary>Owns short admission/completion transactions; never holds a database transaction during speech transport.</summary>
public interface IAudioTranscriptionStore
{
Task<Result<AudioTranscriptionStatusDto>> StatusAsync(Guid userId, Guid audioId, SpeechTranscriptionConfiguration configuration, DateTimeOffset now, CancellationToken ct);
Task<Result<AudioTranscriptionAdmission>> AdmitAsync(Guid userId, Guid audioId, AudioTranscriptionRequestDto request,
SpeechTranscriptionConfiguration configuration, DateTimeOffset now, CancellationToken ct);
Task<Result<AudioTranscriptionReceiptDto>> FinishAsync(Guid userId, Guid attemptId, AudioTranscriptionProviderResult result, DateTimeOffset now, CancellationToken ct);
Task<bool> DispatchAllowedAsync(Guid userId, Guid attemptId, DateTimeOffset now, CancellationToken ct);
Task DeleteOwnerAsync(Guid userId, CancellationToken ct);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface ISourcePortabilityStore
IAsyncEnumerable<SourceBlobChunkExportDto> ChunksAsync(Guid userId, CancellationToken ct);
IAsyncEnumerable<RepresentationDescriptor> RepresentationsAsync(Guid userId, CancellationToken ct);
IAsyncEnumerable<ThinkingAudioExportDto> AudioAnswersAsync(Guid userId, CancellationToken ct);
IAsyncEnumerable<AudioTranscriptionAttemptExportDto> AudioTranscriptionAttemptsAsync(Guid userId, CancellationToken ct);
IAsyncEnumerable<AudioTranscriptionBudgetExportDto> AudioTranscriptionBudgetsAsync(Guid userId, CancellationToken ct);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Taskdeck.Domain.Entities;

namespace Taskdeck.Application.Interfaces;

public interface IWorkspaceAttentionRepository
{
Task<UserPreference> GetAsync(Guid userId, CancellationToken ct);
Task<bool> SaveAsync(Guid userId, long expectedRevision, WorkspaceAttention state, CancellationToken ct);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Taskdeck.Application.Interfaces;

public enum ObservationSaveOutcome { Saved, SourceChanged, StorageBusy }

public interface IWorkspaceInsightRepository
{
Task<IReadOnlyList<WorkspaceMemory>> MemoriesByUserAsync(Guid userId, int limit, int offset, CancellationToken ct);
Expand All @@ -16,4 +18,6 @@ public interface IWorkspaceInsightRepository
void Add(WorkspaceMemory memory);
void GuardMemoryRevision(WorkspaceMemory memory);
Task<bool> SaveAsync(CancellationToken ct);
/// <summary>Revalidates source/access and saves staged observations in one transaction.</summary>
Task<ObservationSaveOutcome> SaveObservationAsync(Guid userId, Guid boardId, Guid cardId, string fingerprint, CancellationToken ct);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Taskdeck.Application.DTOs;

namespace Taskdeck.Application.Interfaces;

/// <summary>Fresh, untracked reads with active-board ownership/membership enforced in the query.</summary>
public interface IWorkspaceObservationReader
{
Task<ObservationSourceDto?> SourceAsync(Guid userId, Guid boardId, Guid cardId, CancellationToken ct);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public AccountDeletionService(
IActiveUserCache? activeUserCache = null,
ILogger<AccountDeletionService>? logger = null,
ICaptureStore? captureStore = null,
IBlobStore? blobStore = null)
IBlobStore? blobStore = null,
IAudioTranscriptionStore? audioTranscription = null)
{
_unitOfWork = unitOfWork;
_historyService = historyService;
Expand All @@ -52,10 +53,12 @@ public AccountDeletionService(
_workspaceInsights = workspaceInsights;
_captureStore = captureStore;
_blobStore = blobStore;
_audioTranscription = audioTranscription;
}

private readonly ICaptureStore? _captureStore;
private readonly IBlobStore? _blobStore;
private readonly IAudioTranscriptionStore? _audioTranscription;

public async Task<Result<AccountDeletionResultDto>> DeleteAccountAsync(
Guid userId,
Expand Down Expand Up @@ -132,6 +135,8 @@ await _historyService.LogActionAsync(
// 3b. Delete the durable Capture mirrors (ADR-0065). Rows exist only when
// ContextFabric:DualWriteCaptures was ever on; they carry user-authored titles and
// their FK to User is Restrict, so they must go inside this same transaction.
if (_audioTranscription is not null)
await _audioTranscription.DeleteOwnerAsync(userId, cancellationToken);
var durableCapturesDeleted = _captureStore is null
? 0
: await _captureStore.DeleteByUserAsync(userId, cancellationToken);
Expand Down
Loading
Loading