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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion android/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,17 @@ core.trackStatementRenewalTargets(
)
```

The ledger persists across launches, and it is append-only: there is no untrack, and an entry is dropped only when the identity that promised it changes. `WalletSso` and `ProductStatementAllowance` are derivation recipes and survive that; `Account` carries a fixed account id and does not, so re-track raw accounts whenever the active identity changes. A pruned target is absent from the report rather than reported as failed. There is no reader and no untrack on this surface: a host cannot list what is tracked, cannot remove a wrong entry, and cannot detect a pruned one except by noticing it missing from a report. Re-tracking is idempotent, so the safe habit is to re-track the full set after every identity change rather than trying to reason about what survived.
The ledger persists across launches, and it is append-only: there is no untrack, and an entry is dropped only when the identity that promised it changes. `WalletSso` and `ProductStatementAllowance` are derivation recipes and survive that; `Account` carries a fixed account id and does not. A dropped target is listed in `report.pruned`, which is how a host learns to re-track one and keep renewal covering it. There is still no reader and no untrack on this surface, so a host cannot list what is tracked or remove a wrong entry. Re-tracking is idempotent, so the safe habit is to re-track the full set after every identity change rather than trying to reason about what survived.

Then run a pass from a `WorkManager` worker. It submits extrinsics and blocks until they are included, so keep it off the main thread. It needs an active session too, which is the whole difficulty here: a worker on a cold start has none until you restore one, and the pass then fails with the bare reason `Disconnected`. Restore the session first, and read that reason as "not ready" rather than as a renewal failure. `startStatementAllowanceRenewal()` does not need this care, since its loop skips a tick with no session and retries.

```kotlin
val report = core.renewStatementAllowances()
report.outcomes.forEach { Log.i(TAG, "${it.label}: ${it.status}") }
report.pruned.forEach {
// Promised by a previous identity and discarded; re-track to keep it renewed.
Log.w(TAG, "dropped: $it")
}
if (report.slotsExhausted) {
// Every slot for this period is taken and none was replaceable.
}
Expand Down
6 changes: 5 additions & 1 deletion ios/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ try runtime.trackStatementRenewalTargets([
])
```

The ledger persists across launches, and it is append-only: there is no untrack, and an entry is dropped only when the identity that promised it changes. `.walletSso` and `.productStatementAllowance` are derivation recipes, so they survive that; `.account` carries a fixed account id and does not. Re-track raw accounts whenever the active identity changes, or renewal quietly stops covering them — a pruned target is absent from the report rather than reported as failed. There is no reader and no untrack on this surface: a host cannot list what is tracked, cannot remove a wrong entry, and cannot detect a pruned one except by noticing it missing from a report. Re-tracking is idempotent, so the safe habit is to re-track the full set after every identity change rather than trying to reason about what survived.
The ledger persists across launches, and it is append-only: there is no untrack, and an entry is dropped only when the identity that promised it changes. `.walletSso` and `.productStatementAllowance` are derivation recipes, so they survive that; `.account` carries a fixed account id and does not. A dropped target is listed in `report.pruned`, which is how a host learns to re-track one and keep renewal covering it. There is still no reader and no untrack on this surface, so a host cannot list what is tracked or remove a wrong entry. Re-tracking is idempotent, so the safe habit is to re-track the full set after every identity change rather than trying to reason about what survived.

Then run a pass from a background task, off the main thread. It needs an active session too, which is the whole difficulty here: a `BGTaskScheduler` wake on a cold start has none until you restore one, and the pass then fails with the bare reason `Disconnected`. Restore the session first, and read that reason as "not ready" rather than as a renewal failure. `startStatementAllowanceRenewal()` does not need this care, since its loop skips a tick with no session and retries.

Expand All @@ -118,6 +118,10 @@ let report = try runtime.renewStatementAllowances()
for outcome in report.outcomes {
log("\(outcome.label): \(outcome.status)")
}
for label in report.pruned {
// Promised by a previous identity and discarded; re-track to keep it renewed.
log("dropped: \(label)")
}
if report.slotsExhausted {
// Every slot for this period is taken and none was replaceable.
}
Expand Down
49 changes: 49 additions & 0 deletions ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4133,6 +4133,17 @@ public struct StatementRenewalReport: Equatable, Hashable {
* Per-target outcomes in ledger order.
*/
public var outcomes: [StatementRenewalOutcome]
/**
* Labels of targets this pass dropped because a different identity
* promised them.
*
* Dropping is silent otherwise: a pruned target simply stops appearing in
* `outcomes`, and the surface has no way to list the ledger, so a host
* could only infer it from an absence. A raw account target does not
* survive a change of root entropy, so this is how a host learns to
* re-track one.
*/
public var pruned: [String]
/**
* Whether the pass hit slot exhaustion for this period.
*/
Expand All @@ -4147,11 +4158,22 @@ public struct StatementRenewalReport: Equatable, Hashable {
/**
* Per-target outcomes in ledger order.
*/outcomes: [StatementRenewalOutcome],
/**
* Labels of targets this pass dropped because a different identity
* promised them.
*
* Dropping is silent otherwise: a pruned target simply stops appearing in
* `outcomes`, and the surface has no way to list the ledger, so a host
* could only infer it from an absence. A raw account target does not
* survive a change of root entropy, so this is how a host learns to
* re-track one.
*/pruned: [String],
/**
* Whether the pass hit slot exhaustion for this period.
*/slotsExhausted: Bool) {
self.period = period
self.outcomes = outcomes
self.pruned = pruned
self.slotsExhausted = slotsExhausted
}

Expand All @@ -4173,13 +4195,15 @@ public struct FfiConverterTypeStatementRenewalReport: FfiConverterRustBuffer {
try StatementRenewalReport(
period: FfiConverterUInt32.read(from: &buf),
outcomes: FfiConverterSequenceTypeStatementRenewalOutcome.read(from: &buf),
pruned: FfiConverterSequenceString.read(from: &buf),
slotsExhausted: FfiConverterBool.read(from: &buf)
)
}

public static func write(_ value: StatementRenewalReport, into buf: inout [UInt8]) {
FfiConverterUInt32.write(value.period, into: &buf)
FfiConverterSequenceTypeStatementRenewalOutcome.write(value.outcomes, into: &buf)
FfiConverterSequenceString.write(value.pruned, into: &buf)
FfiConverterBool.write(value.slotsExhausted, into: &buf)
}
}
Expand Down Expand Up @@ -5801,6 +5825,31 @@ fileprivate struct FfiConverterOptionTypeProductAccountId: FfiConverterRustBuffe
}
}

#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterSequenceString: FfiConverterRustBuffer {
typealias SwiftType = [String]

public static func write(_ value: [String], into buf: inout [UInt8]) {
let len = Int32(value.count)
writeInt(&buf, len)
for item in value {
FfiConverterString.write(item, into: &buf)
}
}

public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [String] {
let len: Int32 = try readInt(&buf)
var seq = [String]()
seq.reserveCapacity(Int(len))
for _ in 0 ..< len {
seq.append(try FfiConverterString.read(from: &buf))
}
return seq
}
}

#if swift(>=5.8)
@_documentation(visibility: private)
#endif
Expand Down
6 changes: 6 additions & 0 deletions rust/crates/truapi-host-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1414,12 +1414,18 @@ async fn run_renew(session: &mut SigningHostSession) -> Result<()> {
TargetRenewalStatus::SkippedExhausted => skipped += 1,
}
}
for label in &report.pruned {
Comment thread
TarikGul marked this conversation as resolved.
terminal_ui::output_event(SystemEvent::AllowanceRenewalPruned {
target: label.clone(),
});
}
terminal_ui::output_event(SystemEvent::AllowanceRenewalReport {
period: report.period,
renewed,
fresh,
failed,
skipped,
pruned: report.pruned.len(),
});

if report.slots_exhausted {
Expand Down
20 changes: 17 additions & 3 deletions rust/crates/truapi-host-cli/src/terminal_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,16 @@ pub enum SystemEvent {
target: String,
reason: String,
},
AllowanceRenewalPruned {
target: String,
},
AllowanceRenewalReport {
period: u32,
renewed: usize,
fresh: usize,
failed: usize,
skipped: usize,
pruned: usize,
},
NotificationDelivered {
id: u32,
Expand Down Expand Up @@ -1348,21 +1352,31 @@ impl App {
Some(reason),
ActivityState::Failed,
),
// A prune is not a failed renewal: nothing was rejected, an entry
// this identity never promised was discarded. Rendering it red beside
// chain rejections reads as an error the host should chase.
SystemEvent::AllowanceRenewalPruned { target } => self.activity(
format!("allowance:{target}"),
format!("{} dropped from the ledger", allowance_name(&target)),
Some("promised by a previous identity; re-track it to keep it renewed".to_string()),
ActivityState::Warning,
),
SystemEvent::AllowanceRenewalReport {
period,
renewed,
fresh,
failed,
skipped,
pruned,
} => {
if renewed + fresh + failed + skipped == 0 {
if renewed + fresh + failed + skipped + pruned == 0 {
self.notice(
NoticeTone::Info,
"No tracked allowance targets".to_string(),
Some(format!("Statement period {period}")),
);
} else {
let tone = if failed + skipped > 0 {
let tone = if failed + skipped + pruned > 0 {
NoticeTone::Warning
} else {
NoticeTone::Success
Expand All @@ -1371,7 +1385,7 @@ impl App {
tone,
"Allowance renewal finished".to_string(),
Some(format!(
"Period {period} · {renewed} renewed · {fresh} fresh · {failed} failed · {skipped} skipped"
"Period {period} · {renewed} renewed · {fresh} fresh · {failed} failed · {skipped} skipped · {pruned} pruned"
)),
);
}
Expand Down
Loading