Uptake agent archiving in Sales Order Agent (AB#644549) - #10308
Uptake agent archiving in Sales Order Agent (AB#644549)#10308Qasim (qasimikram) wants to merge 15 commits into
Conversation
Make the Sales Order Agent app archive-aware now that the platform and System Application support archiving an agent (Substate = Archived). - The 20-instance limit counts active agents only; archived agents no longer occupy a slot. - Display name, initials and mailbox/folder uniqueness ignore archived agents, so an archived agent's identity and mailbox can be reused. - Setup page is read-only for an archived agent, with a notification; setup, instruction sync and manual task creation are guarded. - Upgrade no longer tries to refresh instructions or identity for archived agents, which would fail against the read-only agent. - Dispatcher stagger and the KPI page skip archived agents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The new archived-agent guards in the setup page return immediately from user-invoked handlers without surfacing why the click was ignored. After the initial notification is gone, actions such as "Create task" can degrade into a no-op instead of explicitly telling the user that archived agents are read-only. Disable these affordances when AgentIsArchived, or raise the archived-agent message at the action point. Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4 |
…te/qaikram/soa-archive-uptake
- Consolidate the archived-agent scans on the CountNonArchivedSetups and FindFirstNonArchivedSetup helpers instead of repeating the loop in SOAImpl, SOAKPI and SOA Setup. - Add MustTreatAgentAsArchived, which fails closed when the Agent table cannot be read, and use it for the guards that block an operation. Counting and advisory callers keep the lenient IsAgentArchived. - Use a neutral archived-agent error text, since the guard also blocks manual task creation and not only setup changes. - Disable the create-task and signature affordances for an archived agent and show the archived message instead of silently ignoring the click. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Fixed in d8c2c64. The archived-agent affordances are now visibly disabled rather than silently ignored: |
Agent is a virtual table whose records the platform builds by calling back into the agent metadata provider of the owning app. Reading it with Get from IsManualAgentTaskCreationEnabled therefore re-entered that same method until the stack ran out (NavNCLStackOverflowException). - IsAgentArchived now filters on User Security ID, Agent Metadata Provider and Substate, which are all backed by the agent data table, and only asks whether a record exists. The platform keeps that on the database fast path, so no agent record is built, no metadata callback runs, and the check is a single keyed lookup instead of one lookup plus a rebuild per row. - Filtering on the metadata provider also stops the check from touching archived agents of other apps and running their metadata code. - IsManualAgentTaskCreationEnabled no longer looks the agent up at all. The platform already hides task creation for archived agents, and this method runs for every agent record it builds. CreateManualAgentTask still guards the operation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Pushed 5c8a398 after hitting a What happened. Why the fix is not just "remove the check". The platform distinguishes two access shapes on this table:
Filtering on the metadata provider matters on its own: without it the check reaches archived agents belonging to other apps and executes their metadata code.
Suggested follow-ups outside this PR, since every agent app will hit this:
|
|
UpgradeSOAKPIToPerAgent still seeds the fallback TargetAgentSecurityID with a plain SOASetup.FindFirst(). In tenants where the first setup now belongs to an archived agent, legacy KPI rows without a stored "User Security ID" will be migrated into that archived agent's summary, while the rest of this PR deliberately excludes archived agents from KPI selection and live usage. Use the same non-archived setup selection here that the runtime code now uses, so migrated KPI totals do not end up attached to an agent the UI skips. Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4 |
…their scheduled tasks - The suggested display name and initials for a new agent no longer reuse the identity of an archived agent. Archived agents keep their place in the series, so an archived SO2 next to a live SO3 and SO4 now yields SO5. Reuse is still possible on purpose, because identity validation continues to ignore archived agents and accepts a name typed by the admin. - If a dispatcher or recovery task for an archived agent runs anyway, it now cancels both scheduled tasks, clears them from the setup record and logs usage telemetry, instead of exiting and leaving the task registered. Archiving requires an inactive agent, so this only covers a task that survived deactivation. A deactivated agent is untouched, since it can be reactivated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ActiveSOAgentSetupExists no longer walks every setup record. It is two bounded queries now, which matters because the awareness notification evaluates it for ordinary users working with sales documents. - The two upgrade write paths use MustTreatAgentAsArchived, so an unreadable agent state blocks the write instead of allowing it. This matches the guards used elsewhere for writes. - Cleanup of the scheduled tasks of an inactive agent reports what really happened. Cancelling a task can be refused, so a run that leaves a task behind is logged as an error rather than as a completed removal. - Legacy KPI records are attributed to an agent that is still in use. Attaching them to an archived agent would hide them, because the KPI pages skip archived agents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Fixed in 82ce597. |
- The legacy KPI migration falls back to an archived agent when every agent is archived. Skipping the migration and then deleting the legacy table would have destroyed that history for such a tenant. - Cleanup of the scheduled tasks of an inactive agent captures the call stack of the refused cancellation where it happens, instead of reading the last error after a commit and further record work, when it may describe something else. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The new inactive-agent cleanup path (RemoveScheduledTasksIfAgentNotActive) is documented as never failing the caller, but RemoveScheduledTask still cancels the two IDs stored on the setup record ("Agent Scheduled Task ID" / "Recovery Scheduled Task ID") with a direct, unguarded TaskScheduler.CancelTask call, while the newly-added CancelScheduledTasksForCodeunit path correctly wraps the same call in the new TryCancelTask TryFunction. If either stored task ID cannot be cancelled by the current session (the same permission edge case the new code explicitly handles for the fallback lookup), CancelTask raises and aborts RemoveScheduledTasksIfAgentNotActive before it commits, clears the IDs, or logs telemetry — undermining the "cleanup must never fail the caller" contract stated in the new method's own doc comment. Suggested fix (apply manually — could not be anchored as a one-click suggestion): if TaskScheduler.TaskExists(SOASetup."Agent Scheduled Task ID") then begin
if not TryCancelTask(SOASetup."Agent Scheduled Task ID") then begin
if CancelErrorCallStack = '' then
CancelErrorCallStack := GetLastErrorCallStack();
end else
FeatureTelemetry.LogUsage('0000NGN', SOASetupCU.GetFeatureName(), TelemetryAgentScheduledTaskCancelledLbl, TelemetryDimensions);
end;
if TaskScheduler.TaskExists(SOASetup."Recovery Scheduled Task ID") then begin
if not TryCancelTask(SOASetup."Recovery Scheduled Task ID") then begin
if CancelErrorCallStack = '' then
CancelErrorCallStack := GetLastErrorCallStack();
end else
FeatureTelemetry.LogUsage('0000NGO', SOASetupCU.GetFeatureName(), TelemetryRecoveryScheduledTaskCancelledLbl, TelemetryDimensions);
end;Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4 |
|
Review notes, mostly on placement and layering rather than on the behaviour itself. Happy to be told I'm missing context on any of these. 1.
|
- Every cancellation in the scheduled task cleanup is protected. A refused cancellation of one of the two remembered ids used to fail the run before the ids were cleared, which is the opposite of what the cleanup promises. - Task ids are collected before anything is cancelled, so removing a row cannot skip the record that follows it. - Both upgrade paths check that the agent state is readable before they start. The identity upgrade leaves its tag unset in that case, so a later run can still do the work instead of the migration being marked as done, and both log why nothing happened. An unreadable state and an archived agent are no longer indistinguishable. - The active check treats any substate other than archived as live, so a substate added later does not make live agents look inactive and get their scheduled tasks cancelled. - Closing the setup page of an archived agent explains that nothing was saved. The agent setup part stays editable, so a change made there was discarded without a word. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cancelling a scheduled task deletes its record, and AL does not allow a database write inside a TryFunction, so the protected cancellation added in the previous commit failed at runtime with "Call to the function 'DELETE' is not allowed inside the call to 'CancelScheduledTasksForRecord' when it is used as a TryFunction". The cancellations are back to direct calls, as they were before. The outcome is now established by re-reading the scheduled tasks of the setup record after the cleanup, rather than by inferring it from a caught error, so the telemetry still distinguishes a complete cleanup from one that left something behind, and it reports what is really there. Task ids are still collected before anything is cancelled, so removing a row cannot skip the record that follows it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Correction to my earlier reply on this thread: the protected cancellation I described could not work, and I have replaced it in 5ffdf1b. Wrapping That applies to the suggestion in this thread as well: the two id-based cancellations cannot be protected that way either, and neither could the sweep that was already using the wrapper. What the cleanup does now:
The remaining exposure is the one this thread raised: if a cancellation is ever refused, the run fails before the ids are cleared. That is the behaviour on |
Agentic PR Review - Round 1Recommendation: Accept with SuggestionsWhat this PR doesThe platform The change is coherent and matches the task. The instance limit and identity-suffix search now exclude or skip archived rows; the setup page resolves the archived state on open, becomes read-only, and every write path ( SuggestionsS1 - Refused task cancellation can skip survivor telemetry S2 - New archive-aware branches have no automated coverage in this repo Risk assessment and necessityRisk: The regression surface is agent scheduling and setup, not posting or amounts, so there is no financial exposure. The main behavior change to watch: Necessity: The change is required. Without app-side gates, archived Sales Order Agents keep counting against the 20-instance limit, keep their setup page writable, keep suggesting used identities, and keep scheduled tasks running after deactivation - all real defects the platform does not cover for app-owned data. The scope is appropriate for an uptake task.
|
… interface - The two labels added for the skipped upgrade paths were declared inside the CLEAN29 block, while the code that uses them runs unconditionally, so defining CLEAN29 removed the declarations and left the references behind. Only the legacy KPI label belongs in that block. Verified by compiling with and without CLEAN29. - IsManualAgentTaskCreationEnabled reports an archived agent as unavailable again, so it agrees with CreateManualAgentTask, which errors for the same state. It was removed earlier because reading the agent from that method recursed into the metadata provider, which no longer happens: the archived check asks whether a record with the given filters exists and never builds one, on either of the platform paths for that question. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reinstating the archived check in IsManualAgentTaskCreationEnabled brought back the stack overflow it was removed for. Reading the Agent virtual table with a filtered IsEmpty is not safe from that method either: the platform serves that read by building agent records, and building a record calls the method again. Filtering bounds the work, it does not avoid the materialization. The method is free of agent reads again, and the comments now state the rule without qualification: nothing the platform can invoke while it builds an agent record may read the Agent table in any form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What & why
Uptake of the agent archiving capability (#8967, plus the follow-up fixes in #10032) in the Sales Order Agent app.
Archiving is a one-way soft delete: the agent must be deactivated first, it stays in the database as read-only, and the platform blocks writes to
Agent/AgentData. App-owned tables and app logic are not covered by that gate, so the SOA app has to tell archived agents apart itself.Instance limit counts active agents only
AllowCreateNewSOAgentcounted everySOA Setuprow owned by the user against the limit of 20. An archived agent can never be reactivated, so it no longer occupies a slot.GetSOAUsernamelooked for a free user name only up toMaxSOAInstances() + 10. Archived agents keep their user name forever, so that bound could be exhausted; it now uses the existingMaxAgentIdentitySuffix()(99).Identity and mailbox can be reused, but are not suggested
ValidateAgentIdentity(display name, initials) andCheckMailboxUnique(email account plus folder) ignore archived agents, so an administrator can deliberately reuse the identity and mailbox of an archived agent.The suggested identity for a new agent deliberately does not reuse them. Archived agents keep their place in the series, so an archived
SO2next to a liveSO3andSO4yieldsSO5rather than reusingSO2.Setup experience is read-only for archived agents
The platform still opens the provider setup page for an archived agent. Page 4400 resolves the archived state on open, makes the page non-editable, shows a notification and skips
ApplySetup. The create-task and signature affordances are disabled rather than silently ignored, and the mailbox and folder assist-edits explain why the click did nothing.UpdateAgentandUpdateInstructionsare guarded as defence in depth for non-UI callers.Scheduled tasks do not outlive an active agent
Deactivating an agent from the Agent List or Agent Card never runs SOA code, and archiving is only possible from those pages, so tasks could stay registered and keep running. Two changes:
SOA DispatcherandSOA Recoverycheck first whether the agent is still live. If it is deactivated, archived or gone, they cancel the tasks, clear the ids and stop instead of rescheduling.Scheduled Task(Run Codeunit+Company+Record) rather than only the two ids stored on the setup row, so duplicates and tasks whose id was lost are removed too. SinceRemoveScheduledTaskruns on every dispatcher cycle before rescheduling, accumulated duplicates are cleaned up automatically.A refused cancellation (a session may not cancel a task another user scheduled) does not fail the caller; the task is retried on the next cleanup and the outcome is logged as an error rather than as a completed removal.
Upgrade is archive-aware
AlwaysUpdateAgentInstructionsOnUpgraderuns on every upgrade with no upgrade tag and callsAgent.SetInstructionsfor every setup row. For an archived agent that fails, is swallowed by theTryFunction, and logs warning telemetry0000U1Pon every upgrade forever. Archived agents are skipped, as they are inUpgradeAgentIdentity. Both are write paths, so they use the fail-closed check.UpgradeSOAKPIToPerAgentattributes legacy KPI rows to a non-archived agent, because the KPI pages skip archived agents. If every agent is archived it falls back to the first setup, since the legacy table is deleted afterwards and skipping would destroy the history.Smaller archive-aware fixes
SOA Impl.GetInstanceOffsetcounted archived siblings when staggering dispatcher start times, pushing live agents further out over time.SOA KPI.GetCurrentUserAgentSecurityIDcould pick an archived agent and open a dead KPI page.SOA Setuprow existed. An environment left with only archived agents is now treated like one that never had an agent, in two bounded queries because that path runs for ordinary users.Reading archived state safely
Agentis a virtual table. The platform builds each record by calling back into the owning app's agent metadata provider (GetInitials,GetSetupPageId,IsManualAgentTaskCreationEnabledand others). Any read of that table,Get,FindSet,Countor a filteredIsEmpty, is served by building records, so a read from one of those methods re-enters it until the stack runs out. Two revisions of this PR did that and produced aNavNCLStackOverflowException; filtering bounds the work but does not avoid the materialisation.The rule this app follows is therefore absolute: nothing the platform can invoke while it builds an agent record may read the
Agenttable in any form.IsManualAgentTaskCreationEnabledis kept free of agent reads for that reason, and the platform already excludes archived agents from task creation throughCanCurrentUserUseAgent.CreateManualAgentTaskruns from the task pane action, outside record building, so it does guard the operation.IsAgentArchivedandIsAgentActiveare called only from outside that path. They filter onUser Security ID,Agent Metadata ProviderandSubstate/State, which keeps each check to the single agent it is about rather than the whole table, and stops it from materialising archived agents belonging to other apps.Two probes exist on purpose.
IsAgentArchivedis lenient: a session that cannot read theAgenttable sees "not archived", so counting and advisory callers are never blocked.MustTreatAgentAsArchivedfails closed and backs every guard that blocks an operation, so an unreadable state cannot be used to bypass the read-only rule.Telemetry
Two new events on the scheduled-task cleanup path:
0000V3Kwhen every task was removed,0000V3Lwhen one could not be cancelled and was left for the next attempt.Linked work
AB#644549
How I validated this
What I tested and the outcome
Sales Order Agentwith the 18.0 AL compiler against the 29.0 symbol set, with the repobase.ruleset.jsonplus CodeCop and UICop: clean, no errors and no new warnings.SOA Recoverytasks, which is what motivated the lookup-based cancellation.Agenttable cannot be read.Risk & compatibility
Agent.Substatefrom Ability to archive agents #8967. No newapp.jsondependency was needed.